简体   繁体   English

如何在Dapper.Net中编写一对多查询?

[英]How do I write one to many query in Dapper.Net?

I've written this code to project one to many relation but it's not working: 我已经编写了这段代码来计划一对多关系,但是它不起作用:

using (var connection = new SqlConnection(connectionString))
{
   connection.Open();

   IEnumerable<Store> stores = connection.Query<Store, IEnumerable<Employee>, Store>
                        (@"Select Stores.Id as StoreId, Stores.Name, 
                                  Employees.Id as EmployeeId, Employees.FirstName,
                                  Employees.LastName, Employees.StoreId 
                           from Store Stores 
                           INNER JOIN Employee Employees ON Stores.Id = Employees.StoreId",
                        (a, s) => { a.Employees = s; return a; }, 
                        splitOn: "EmployeeId");

   foreach (var store in stores)
   {
       Console.WriteLine(store.Name);
   }
}

Can anybody spot the mistake? 有人可以发现错误吗?

EDIT: 编辑:

These are my entities: 这些是我的实体:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public IList<Store> Stores { get; set; }

    public Product()
    {
        Stores = new List<Store>();
    }
}

public class Store
{
    public int Id { get; set; }
    public string Name { get; set; }
    public IEnumerable<Product> Products { get; set; }
    public IEnumerable<Employee> Employees { get; set; }

    public Store()
    {
        Products = new List<Product>();
        Employees = new List<Employee>();
    }
}

EDIT: 编辑:

I change the query to: 我将查询更改为:

IEnumerable<Store> stores = connection.Query<Store, List<Employee>, Store>
        (@"Select Stores.Id as StoreId ,Stores.Name,Employees.Id as EmployeeId,
           Employees.FirstName,Employees.LastName,Employees.StoreId 
           from Store Stores INNER JOIN Employee Employees 
           ON Stores.Id = Employees.StoreId",
         (a, s) => { a.Employees = s; return a; }, splitOn: "EmployeeId");

and I get rid of exceptions! 我摆脱了例外! However, Employees are not mapped at all. 但是,员工完全没有映射。 I am still not sure what problem it had with IEnumerable<Employee> in first query. 我仍然不确定在第一次查询中IEnumerable<Employee>有什么问题。

This post shows how to query a highly normalised SQL database , and map the result into a set of highly nested C# POCO objects. 这篇文章展示了如何查询高度标准化的SQL数据库 ,以及如何将结果映射到一组高度嵌套的C#POCO对象中。

Ingredients: 配料:

  • 8 lines of C#. 8行C#。
  • Some reasonably simple SQL that uses some joins. 一些使用一些联接的相当简单的SQL。
  • Two awesome libraries. 两个很棒的库。

The insight that allowed me to solve this problem is to separate the MicroORM from mapping the result back to the POCO Entities . 使我能够解决此问题的见解是将MicroORMmapping the result back to the POCO Entities分开。 Thus, we use two separate libraries: 因此,我们使用两个单独的库:

Essentially, we use Dapper to query the database, then use Slapper.Automapper to map the result straight into our POCOs. 本质上,我们使用Dapper查询数据库,然后使用Slapper.Automapper将结果直接映射到我们的POCO中。

Advantages 好处

  • Simplicity . 简单性 Its less than 8 lines of code. 它少于8行代码。 I find this a lot easier to understand, debug, and change. 我发现这更容易理解,调试和更改。
  • Less code . 更少的代码 A few lines of code is all Slapper.Automapper needs to handle anything you throw at it, even if we have a complex nested POCO (ie POCO contains List<MyClass1> which in turn contains List<MySubClass2> , etc). Slapper.Automapper只需编写几行代码,即使我们有一个复杂的嵌套POCO(即POCO包含List<MyClass1> ,而List<MyClass1>依次包含List<MySubClass2>等), Automapper也需要处理所有抛出的问题。
  • Speed . 速度 Both of these libraries have an extraordinary amount of optimization and caching to make them run almost as fast as hand tuned ADO.NET queries. 这两个库都进行了大量的优化和缓存,以使其运行速度几乎与手动调整的ADO.NET查询一样快。
  • Separation of concerns . 关注点分离 We can change the MicroORM for a different one, and the mapping still works, and vice-versa. 我们可以将MicroORM更改为另一种,映射仍然有效,反之亦然。
  • Flexibility . 灵活性强 Slapper.Automapper handles arbitrarily nested hierarchies, it isn't limited to a couple of levels of nesting. Slapper.Automapper处理任意嵌套的层次结构,它不仅限于几个嵌套级别。 We can easily make rapid changes, and everything will still work. 我们可以轻松进行快速更改,并且一切仍然可以进行。
  • Debugging . 调试 We can first see that the SQL query is working properly, then we can check that the SQL query result is properly mapped back to the target POCO Entities. 我们首先可以看到SQL查询工作正常,然后可以检查SQL查询结果是否正确映射回目标POCO实体。
  • Ease of development in SQL . 易于SQL开发 I find that creating flattened queries with inner joins to return flat results is much easier than creating multiple select statements, with stitching on the client side. 我发现使用inner joins创建扁平化查询以返回扁平化结果要比创建多个select语句(在客户端进行拼接)要容易得多。
  • Optimized queries in SQL . SQL中的优化查询 In a highly normalized database, creating a flat query allows the SQL engine to apply advanced optimizations to the whole which would not normally be possible if many small individual queries were constructed and run. 在高度规范化的数据库中,创建平面查询使SQL引擎可以对整体应用高级优化,如果构造并运行了许多小的单个查询,通常这是不可能的。
  • Trust . 信任 Dapper is the back end for StackOverflow, and, well, Randy Burden is a bit of a superstar. Dapper是StackOverflow的后端,而且,Randy Burden有点像超级巨星。 Need I say any more? 我还要说什么吗?
  • Speed of development. 发展速度。 I was able to do some extraordinarily complex queries, with many levels of nesting, and the dev time was quite low. 我能够进行很多嵌套的异常复杂的查询,并且开发时间很短。
  • Fewer bugs. 更少的错误。 I wrote it once, it just worked, and this technique is now helping to power a FTSE company. 我曾经写过它,但是它确实起作用了,这种技术现在正在帮助为FTSE公司提供动力。 There was so little code that there was no unexpected behavior. 几乎没有代码,因此没有意外行为。

Disadvantages 缺点

  • Scaling beyond 1,000,000 rows returned. 返回超过1,000,000行的规模。 Works well when returning < 100,000 rows. 返回<100,000行时效果很好。 However, if we are bringing back >1,000,000 rows, in order to reduce the traffic between us and SQL server, we should not flatten it out using inner join (which brings back duplicates), we should instead use multiple select statements and stitch everything back together on the client side (see the other answers on this page). 但是,如果我们要返回> 1,000,000行,为了减少我们和SQL Server之间的通信量,我们不应该使用inner join联接(它会带回重复项)将其弄平,而应该使用多个select语句并将所有内容缝起来一起在客户端(请参阅此页面上的其他答案)。
  • This technique is query oriented . 此技术是面向查询的 I haven't used this technique to write to the database, but I'm sure that Dapper is more than capable of doing this with some more extra work, as StackOverflow itself uses Dapper as its Data Access Layer (DAL). 我没有使用这种技术来写数据库,但是我敢肯定Dapper可以完成更多的工作,因为StackOverflow本身使用Dapper作为其数据访问层(DAL)。

Performance Testing 性能测试

In my tests, Slapper.Automapper added a small overhead to the results returned by Dapper, which meant that it was still 10x faster than Entity Framework, and the combination is still pretty darn close to the theoretical maximum speed SQL + C# is capable of . 在我的测试中, Slapper.Automapper给Dapper返回的结果增加了很小的开销,这意味着它仍然比Entity Framework快10倍,并且组合仍然相当接近SQL + C#能够达到的理论最大速度

In most practical cases, most of the overhead would be in a less-than-optimum SQL query, and not with some mapping of the results on the C# side. 在大多数实际情况下,大部分开销将出现在非最佳SQL查询中,而不是在C#端对结果进行一些映射。

Performance Testing Results 性能测试结果

Total number of iterations: 1000 迭代总数:1000

  • Dapper by itself : 1.889 milliseconds per query, using 3 lines of code to return the dynamic . Dapper by itself :每个查询1.889毫秒,使用3 lines of code to return the dynamic
  • Dapper + Slapper.Automapper : 2.463 milliseconds per query, using an additional 3 lines of code for the query + mapping from dynamic to POCO Entities . Dapper + Slapper.Automapper :每次查询2.463毫秒,使用额外的3 lines of code for the query + mapping from dynamic to POCO Entities

Worked Example 工作的例子

In this example, we have list of Contacts , and each Contact can have one or more phone numbers . 在此示例中,我们具有Contacts列表,并且每个Contact可以具有一个或多个phone numbers

POCO Entities POCO实体

public class TestContact
{
    public int ContactID { get; set; }
    public string ContactName { get; set; }
    public List<TestPhone> TestPhones { get; set; }
}

public class TestPhone
{
    public int PhoneId { get; set; }
    public int ContactID { get; set; } // foreign key
    public string Number { get; set; }
}

SQL Table TestContact SQL表TestContact

在此处输入图片说明

SQL Table TestPhone SQL表TestPhone

Note that this table has a foreign key ContactID which refers to the TestContact table (this corresponds to the List<TestPhone> in the POCO above). 请注意,此表有一个外键ContactID ,它引用了TestContact表(与上面POCO中的List<TestPhone>相对应)。

在此处输入图片说明

SQL Which Produces Flat Result 产生固定结果的SQL

In our SQL query, we use as many JOIN statements as we need to get all of the data we need, in a flat, denormalized form . 在我们的SQL查询中,我们使用了尽可能多的JOIN语句来获取所需的所有数据(以扁平化,非规范化的形式) Yes, this might produce duplicates in the output, but these duplicates will be eliminated automatically when we use Slapper.Automapper to automatically map the result of this query straight into our POCO object map. 是的,这可能会在输出中产生重复项,但是当我们使用Slapper.Automapper将查询结果直接映射到POCO对象映射中时,这些重复项将被自动消除。

USE [MyDatabase];
    SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId

在此处输入图片说明

C# code C#代码

const string sql = @"SELECT tc.[ContactID] as ContactID
          ,tc.[ContactName] as ContactName
          ,tp.[PhoneId] AS TestPhones_PhoneId
          ,tp.[ContactId] AS TestPhones_ContactId
          ,tp.[Number] AS TestPhones_Number
          FROM TestContact tc
    INNER JOIN TestPhone tp ON tc.ContactId = tp.ContactId";

string connectionString = // -- Insert SQL connection string here.

using (var conn = new SqlConnection(connectionString))
{
    conn.Open();    
    // Can set default database here with conn.ChangeDatabase(...)
    {
        // Step 1: Use Dapper to return the  flat result as a Dynamic.
        dynamic test = conn.Query<dynamic>(sql);

        // Step 2: Use Slapper.Automapper for mapping to the POCO Entities.
        // - IMPORTANT: Let Slapper.Automapper know how to do the mapping;
        //   let it know the primary key for each POCO.
        // - Must also use underscore notation ("_") to name parameters in the SQL query;
        //   see Slapper.Automapper docs.
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestContact), new List<string> { "ContactID" });
        Slapper.AutoMapper.Configuration.AddIdentifiers(typeof(TestPhone), new List<string> { "PhoneID" });

        var testContact = (Slapper.AutoMapper.MapDynamic<TestContact>(test) as IEnumerable<TestContact>).ToList();      

        foreach (var c in testContact)
        {                               
            foreach (var p in c.TestPhones)
            {
                Console.Write("ContactName: {0}: Phone: {1}\n", c.ContactName, p.Number);   
            }
        }
    }
}

Output 输出量

在此处输入图片说明

POCO Entity Hierarchy POCO实体层次结构

Looking in Visual Studio, We can see that Slapper.Automapper has properly populated our POCO Entities, ie we have a List<TestContact> , and each TestContact has a List<TestPhone> . 在Visual Studio中查看,我们可以看到Slapper.Automapper已正确填充了POCO实体,即,我们有一个List<TestContact> ,每个TestContact都有一个List<TestPhone>

在此处输入图片说明

Notes 笔记

Both Dapper and Slapper.Automapper cache everything internally for speed. Dapper和Slapper.Automapper都在内部缓存所有内容以提高速度。 If you run into memory issues (very unlikely), ensure that you occasionally clear the cache for both of them. 如果遇到内存问题(极不可能),请确保偶尔清除两者的缓存。

Ensure that you name the columns coming back, using the underscore ( _ ) notation to give Slapper.Automapper clues on how to map the result into the POCO Entities. 确保使用下划线( _ )表示法来命名返回的列,从而为Slapper.Automapper提供有关如何将结果映射到POCO实体的线索。

Ensure that you give Slapper.Automapper clues on the primary key for each POCO Entity (see the lines Slapper.AutoMapper.Configuration.AddIdentifiers ). 确保为每个POCO实体的主键提供Slapper.Automapper线索(请参见Slapper.AutoMapper.Configuration.AddIdentifiers行)。 You can also use Attributes on the POCO for this. 您也可以为此在POCO上使用Attributes If you skip this step, then it could go wrong (in theory), as Slapper.Automapper would not know how to do the mapping properly. 如果跳过此步骤,则理论上可能会出错,因为Slapper.Automapper将不知道如何正确执行映射。

Update 2015-06-14 更新2015-06-14

Successfully applied this technique to a huge production database with over 40 normalized tables. 成功地将此技术应用于具有40多个标准化表的庞大生产数据库。 It worked perfectly to map an advanced SQL query with over 16 inner join and left join into the proper POCO hierarchy (with 4 levels of nesting). 它完美地映射了一个高级SQL查询,该查询具有16个以上的inner join联接和left join联接到正确的POCO层次结构(具有4个嵌套级别)。 The queries are blindingly fast, almost as fast as hand coding it in ADO.NET (it was typically 52 milliseconds for the query, and 50 milliseconds for the mapping from the flat result into the POCO hierarchy). 查询的速度非常快,几乎与在ADO.NET中手动编码的速度一样快(查询通常为52毫秒,从平面结果到POCO层次结构的映射通常为50毫秒)。 This is really nothing revolutionary, but it sure beats Entity Framework for speed and ease of use, especially if all we are doing is running queries. 这确实没有什么革命性的,但是它确实在速度和易用性方面优于Entity Framework,特别是如果我们正在做的是运行查询。

Update 2016-02-19 更新2016-02-19

Code has been running flawlessly in production for 9 months. 代码在生产中完美运行了9个月。 The latest version of Slapper.Automapper has all of the changes that I applied to fix the issue related to nulls being returned in the SQL query. Slapper.Automapper的最新版本具有我为解决与SQL查询中返回的null相关的问题而应用的所有更改。

Update 2017-02-20 更新2017-02-20

Code has been running flawlessly in production for 21 months, and has handled continuous queries from hundreds of users in a FTSE 250 company. 代码已经在生产中完美运行了21个月,并且已经处理了FTSE 250公司中数百名用户的连续查询。

Slapper.Automapper is also great for mapping a .csv file straight into a list of POCOs. Slapper.Automapper也非常适合将.csv文件直接映射到POCO列表中。 Read the .csv file into a list of IDictionary, then map it straight into the target list of POCOs. 将.csv文件读取到IDictionary列表中,然后将其直接映射到POCO的目标列表中。 The only trick is that you have to add a propery int Id {get; set} 唯一的技巧是您必须添加一个属性int Id {get; set} int Id {get; set} , and make sure it's unique for every row (or else the automapper won't be able to distinguish between the rows). int Id {get; set} ,并确保每一行都是唯一的(否则自动映射器将无法区分行)。

Update 2019-01-29 更新2019-01-29

Minor update to add more code comments. 较小的更新以添加更多代码注释。

See: https://github.com/SlapperAutoMapper/Slapper.AutoMapper 参见: https : //github.com/SlapperAutoMapper/Slapper.AutoMapper

I wanted to keep it as simple as possible, my solution: 我想让它尽可能简单,我的解决方案:

public List<ForumMessage> GetForumMessagesByParentId(int parentId)
{
    var sql = @"
    select d.id_data as Id, d.cd_group As GroupId, d.cd_user as UserId, d.tx_login As Login, 
        d.tx_title As Title, d.tx_message As [Message], d.tx_signature As [Signature], d.nm_views As Views, d.nm_replies As Replies, 
        d.dt_created As CreatedDate, d.dt_lastreply As LastReplyDate, d.dt_edited As EditedDate, d.tx_key As [Key]
    from 
        t_data d
    where d.cd_data = @DataId order by id_data asc;

    select d.id_data As DataId, di.id_data_image As DataImageId, di.cd_image As ImageId, i.fl_local As IsLocal
    from 
        t_data d
        inner join T_data_image di on d.id_data = di.cd_data
        inner join T_image i on di.cd_image = i.id_image 
    where d.id_data = @DataId and di.fl_deleted = 0 order by d.id_data asc;";

    var mapper = _conn.QueryMultiple(sql, new { DataId = parentId });
    var messages = mapper.Read<ForumMessage>().ToDictionary(k => k.Id, v => v);
    var images = mapper.Read<ForumMessageImage>().ToList();

    foreach(var imageGroup in images.GroupBy(g => g.DataId))
    {
        messages[imageGroup.Key].Images = imageGroup.ToList();
    }

    return messages.Values.ToList();
}

I still do one call to the database, and while i now execute 2 queries instead of one, the second query is using a INNER join instead of a less optimal LEFT join. 我仍然对数据库进行一次调用,现在我执行2个查询而不是一个查询,而第二个查询使用的是INNER联接,而不是不太理想的LEFT联接。

A slight modification of Andrew's answer that utilizes a Func to select the parent key instead of GetHashCode . 对Andrew答案的略微修改,它利用Func选择了父键而不是GetHashCode

public static IEnumerable<TParent> QueryParentChild<TParent, TChild, TParentKey>(
    this IDbConnection connection,
    string sql,
    Func<TParent, TParentKey> parentKeySelector,
    Func<TParent, IList<TChild>> childSelector,
    dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
{
    Dictionary<TParentKey, TParent> cache = new Dictionary<TParentKey, TParent>();

    connection.Query<TParent, TChild, TParent>(
        sql,
        (parent, child) =>
            {
                if (!cache.ContainsKey(parentKeySelector(parent)))
                {
                    cache.Add(parentKeySelector(parent), parent);
                }

                TParent cachedParent = cache[parentKeySelector(parent)];
                IList<TChild> children = childSelector(cachedParent);
                children.Add(child);
                return cachedParent;
            },
        param as object, transaction, buffered, splitOn, commandTimeout, commandType);

    return cache.Values;
}

Example usage 用法示例

conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)

According to this answer there is no one to many mapping support built into Dapper.Net. 根据此答案 ,Dapper.Net中没有内置的一对多映射支持。 Queries will always return one object per database row. 查询将始终为每个数据库行返回一个对象。 There is an alternative solution included, though. 不过,还有一个替代解决方案。

Here is a crude workaround 这是一个粗略的解决方法

    public static IEnumerable<TOne> Query<TOne, TMany>(this IDbConnection cnn, string sql, Func<TOne, IList<TMany>> property, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null)
    {
        var cache = new Dictionary<int, TOne>();
        cnn.Query<TOne, TMany, TOne>(sql, (one, many) =>
                                            {
                                                if (!cache.ContainsKey(one.GetHashCode()))
                                                    cache.Add(one.GetHashCode(), one);

                                                var localOne = cache[one.GetHashCode()];
                                                var list = property(localOne);
                                                list.Add(many);
                                                return localOne;
                                            }, param as object, transaction, buffered, splitOn, commandTimeout, commandType);
        return cache.Values;
    }

its by no means the most efficient way, but it will get you up and running. 它绝不是最有效的方法,但是它将帮助您启动并运行。 I'll try and optimise this when i get a chance. 如果有机会,我会尝试并对此进行优化。

use it like this: 像这样使用它:

conn.Query<Product, Store>("sql here", prod => prod.Stores);

bear in mind your objects need to implement GetHashCode , perhaps like this: 请记住,您的对象需要实现GetHashCode ,也许像这样:

    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }

Here is another method: 这是另一种方法:

Order (one) - OrderDetail (many) 订单(一个)-OrderDetail(许多)

using (var connection = new SqlCeConnection(connectionString))
{           
    var orderDictionary = new Dictionary<int, Order>();

    var list = connection.Query<Order, OrderDetail, Order>(
        sql,
        (order, orderDetail) =>
        {
            Order orderEntry;

            if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry))
            {
                orderEntry = order;
                orderEntry.OrderDetails = new List<OrderDetail>();
                orderDictionary.Add(orderEntry.OrderID, orderEntry);
            }

            orderEntry.OrderDetails.Add(orderDetail);
            return orderEntry;
        },
        splitOn: "OrderDetailID")
    .Distinct()
    .ToList();
}

Source : http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many 来源http : //dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM