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

94
我编写了以下代码来实现一对多的关系,但是它没有起作用:
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);
   }
}

有人能发现错误吗?

编辑:

这些是我的实体:

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>();
    }
}

编辑:

我将查询更改为:

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");

我摆脱了异常!但是员工根本没有被映射。我仍然不确定第一个查询在 IEnumerable<Employee> 方面有什么问题。


1
你的实体长什么样? - gideon
2
出了什么问题?你是否遇到了异常?还是得到了意外的结果? - driis
1
错误信息并不具有实际意义,所以我没有费心将其发布。我得到的错误信息是:“{"Value cannot be null.\r\nParameter name: con"}”。在 SqlMapper 中引发错误的代码行是:“il.Emit(OpCodes.Newobj, type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null));”。 - TCM
6个回答

175
这篇文章展示了如何查询一个高度规范化的SQL数据库,并将结果映射到一组高度嵌套的C# POCO对象中。
材料:
- 8行C#代码。 - 一些相对简单的SQL,使用了一些连接。 - 两个很棒的库。
让我解决这个问题的见解是将MicroORM将结果映射回POCO实体分开。因此,我们使用两个不同的库:
- Dapper作为MicroORM。 - Slapper.Automapper用于映射。

本质上,我们使用Dapper查询数据库,然后使用Slapper.Automapper将结果直接映射到我们的POCOs。

优点

  • 简洁性。代码不超过8行,更容易理解、调试和修改。
  • 少量代码。只需几行代码,Slapper.Automapper 就可以处理任何你提供的内容,即使我们有一个复杂的嵌套 POCO(例如,POCO 包含 List<MyClass1>,其中又包含 List<MySubClass2> 等)。
  • 速度。这两个库都具有非常多的优化和缓存功能,使它们的运行速度几乎与手动调整的 ADO.NET 查询一样快。
  • 关注点分离。我们可以更换 MicroORM 为其他 ORM,映射仍然有效,反之亦然。
  • 灵活性Slapper.Automapper 处理任意嵌套层次结构,不限于几个嵌套级别。我们可以轻松地进行快速更改,一切仍将正常工作。
  • 调试。我们可以先查看 SQL 查询是否正常工作,然后再检查 SQL 查询结果是否正确映射回目标 POCO 实体。
  • 在 SQL 中开发的便利性。我发现使用 inner join 创建扁平化查询以返回扁平结果比创建多个选择语句并在客户端上进行拼接更容易。
  • 优化的 SQL 查询。在高度规范化的数据库中,创建一个扁平查询允许 SQL 引擎对整个查询应用高级优化,如果构建和运行许多小型单独的查询,则通常不可能实现这种优化。
  • 信任。Dapper 是 StackOverflow 的后端,Randy Burden 也是一位超级明星。还需要说什么吗?
  • 开发速度。我能够处理一些非常复杂的查询,包含多层嵌套,但开发时间相当短。
  • 更少的错误。我只写了一次,它就正常工作了,而且这种技术现在正在帮助推动一个 FTSE 公司的发展。代码如此之少,没有出现意外行为。

缺点

  • 返回超过1,000,000行时的扩展性问题。当返回 < 100,000 行时,效果很好。然而,如果我们要返回 >1,000,000 行,则为了减少我们与 SQL 服务器之间的流量,我们不应该使用 inner join(它会带回重复数据),而应该使用多个 select 语句,并在客户端上将所有内容拼接在一起(请参见此页面上的其他答案)。
  • 这种技术是面向查询的。我没有使用这种技术来写入数据库,但我相信 Dapper 可以通过更多额外的工作来做到这一点,因为 StackOverflow 本身使用 Dapper 作为其数据访问层(DAL)。

性能测试

在我的测试中,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.
Performance Testing Results
Total number of iterations: 1000
- Dapper by itself: 1.889 milliseconds per query, using 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.
Worked Example In this example, we have a list of Contacts, and each Contact can have one or more phone numbers.
POCO Entities
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表 TestContact

enter image description here

SQL 表格 TestPhone

请注意,此表格具有一个外键 ContactID,它引用了 TestContact 表格(这对应于上面 POCO 中的 List<TestPhone>)。

enter image description here

SQL生成平面结果

在我们的SQL查询中,我们使用尽可能多的JOIN语句来获取所有需要的数据,以平面、非规范化形式呈现。是的,这可能会在输出中产生重复项,但是当我们使用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

enter image description here

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);   
            }
        }
    }
}

输出

enter image description here

POCO实体层次结构

在Visual Studio中查看,我们可以看到Slapper.Automapper已经正确填充了我们的POCO实体,即我们有一个List<TestContact>,每个TestContact都有一个List<TestPhone>

enter image description here

注意事项

Dapper和Slapper.Automapper都在内部缓存所有内容以提高速度。如果遇到内存问题(很少见),请确保定期清除它们的缓存。

请使用下划线 (_) 符号命名返回的列,以便为Slapper.Automapper提供将结果映射到POCO实体的线索。

请为每个POCO实体提供主键的线索,例如在行Slapper.AutoMapper.Configuration.AddIdentifiers中。您也可以在POCO上使用Attributes来完成此操作。如果跳过此步骤,则可能会出现问题(理论上),因为Slapper.Automapper不知道如何正确进行映射。

更新于2015-06-14

成功地将这种技术应用于一个拥有超过40个规范化表的大型生产数据库。它完美地映射了一个包含16个内连接和左连接的高级SQL查询到正确的POCO层次结构中(包含4个嵌套级别)。这些查询速度极快,几乎和在ADO.NET中手动编码一样快(通常查询时间为52毫秒,从平面结果映射到POCO层次结构的时间为50毫秒)。这确实不是什么革命性的东西,但它肯定比Entity Framework更快、更易于使用,特别是如果我们只是运行查询。

更新2016-02-19

代码已经在生产环境中无缺陷地运行了9个月。最新版本的Slapper.Automapper已经包含了我应用于修复与SQL查询返回空值相关的问题的所有更改。

更新2017-02-20

代码已经在生产环境中无缺陷地运行了21个月,并且已经处理了来自FTSE 250公司数百名用户的持续查询。

Slapper.Automapper 也非常适合将 .csv 文件直接映射到 POCO 列表。先将 .csv 文件读入 IDictionary 列表,然后将其直接映射到目标 POCO 列表。唯一的技巧是,您必须添加一个属性 int Id {get; set},并确保对于每一行它都是唯一的(否则 automapper 将无法区分行)。

2019-01-29 更新

微小的更新以添加更多代码注释。

请参阅:https://github.com/SlapperAutoMapper/Slapper.AutoMapper


1
我真的非常不喜欢你所有SQL中的表名前缀约定,它不支持像Dapper的“splitOn”这样的东西。 - tbone
3
这种表名约定是Slapper.Automapper所必需的。是的,Dapper确实支持将映射直接到POCOs,但我更喜欢使用Slapper.Automapper,因为代码如此简洁且易于维护。 - Contango
3
如果不需要给每一列都起别名,我会选择使用Slapper - 就像在你的例子中一样,我希望能够这样说:splitOn: "PhoneId" - 这难道不比给每个字段都起别名要简单得多吗? - tbone
1
我真的很喜欢Slapper的外观,只是想知道您是否尝试过左连接当一个人没有联系电话时?您有处理这个问题的好方法吗? - undefined
1
嗨,我正在ASP.NET MVC Core 2中使用Slapper.Automapper,并收到以下错误 {System.TypeLoadException:无法从程序集'mscorlib,Version = 4.0.0.0,Culture = neutral,PublicKeyToken = b77a5c561934e089'中加载类型'System.Runtime.Remoting.Messaging.CallContext'。 在Slapper.AutoMapper.InternalHelpers.InternalContextStorage.Get[T](String key) - Raj Kumar
显示剩余23条评论

24

我希望保持尽可能简单,我的解决方案:

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();
}

我仍然只向数据库发出一个调用,虽然现在我执行两个查询而不是一个,但第二个查询使用了更优的INNER JOIN而不是不太优化的LEFT JOIN。


6
我喜欢这种方法。纯粹而考虑到我个人意见,映射更易理解。 - Avner
1
似乎这很容易放入一个扩展方法中,该方法需要使用一对lambda表达式,一个用于键选择器,另一个用于子选择器。类似于.Join()但是生成的是对象图而不是扁平化结果。 - AaronLS

10

对安德鲁答案的轻微修改,利用 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;
}

使用示例

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

需要注意的是,使用这种解决方案时,父类负责实例化子属性。class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } } - Clay
1
这个解决方案非常出色,对我们很有效。我确实不得不添加一个检查children.add以检查是否为空,以防没有返回子行。 - tlbignerd
这非常有趣...如果父类不实例化Children,那么即使你通过children变量在内部实例化它,Children也将始终为null。children应该是对Parent.Children的引用,因此对children所做的任何更改都应影响到Parent.Children,或者我错过了什么? - Jonas Stawski

8

根据这个答案,Dapper.Net 中没有内置的一对多映射支持。查询将始终返回每个数据库行的一个对象。不过,其中包括另一种替代方案。


很抱歉,我不明白如何在我的查询中使用它?它试图在没有连接的情况下两次查询数据库(并在示例中使用硬编码的1)。该示例仅返回一个主实体,该实体包含子实体。在我的情况下,我想要投影连接(内部包含列表)。如何使用您提到的链接来实现这一点?在链接中,当行说:(contact, phones) => { contact.Phones = phones; } 我需要为联系人ID与联系人的联系人ID匹配的电话编写过滤器。这非常低效。 - TCM
@Anthony 请看一下Mike的回答。他使用单个查询执行了两个结果集,并使用Map方法将它们连接起来。当然,在你的情况下,你不需要硬编码该值。我会尝试在几个小时内提供一个示例。 - Damir Arh
1
好的,我终于搞定了。谢谢!不知道这样两次查询数据库会对性能产生什么影响,而这本来可以通过单个连接完成。 - TCM
2
还有,如果有三个表,我不明白需要做哪些更改 :p - TCM
1
这太糟糕了...为什么要避免使用连接操作? - GorillaApe
因为联接(join)总是意味着我们正在重复数据,并且查询返回的信息中存在冗余信息。如果我们拥有一个极快的局域网(LAN)并且在本地网络上运行业务应用(Line of Business, LOB),那么这不是问题。我们让SQL服务器执行联接(join),然后将数据传回客户端。但是,如果我们在互联网上运行,带宽非常珍贵,我们不想有任何重复的数据,因此我们在客户端上执行联接(join)。Dapper不喜欢在数据库端进行联接(join),因为它是为基于互联网的StackOverflow编写的。 - Contango

6
这里还有另一种方法:
订单(一)- 订单详情(多)
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();
}

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

这是一个关于Dapper多映射查询(one-to-many)的示例。

3

这是一个简单的解决方法

    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;
    }

这并不是最高效的方法,但能让你运行起来。我会在有机会时尝试优化它。

使用方法如下:

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

牢记你的对象需要实现GetHashCode方法,可能像这样:
    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }

12
缓存实现存在缺陷。哈希码不是唯一的 - 两个对象可能具有相同的哈希码。这可能会导致对象列表被填充了属于另一个对象的项目。 - stmax

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接