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


80

我已经编写了这段代码来设计一对多关系,但是它不起作用:

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'2

1
该错误没有意义,这就是为什么我不愿意发布它。我得到:“ {”值不能为空。\ r \ n参数名称:con“}”。在SqlMapper中引发错误的行是:“ il.Emit(OpCodes.Newobj,type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,null,Type.EmptyTypes,null)));”“
中医

Answers:


162

这篇文章展示了如何查询高度规范化的SQL数据库,以及如何将结果映射到一组高度嵌套的C#POCO对象中。

配料:

  • 8行C#。
  • 一些使用某些联接的相当简单的SQL。
  • 两个很棒的库。

这让我解决这个问题的见解是分开MicroORMmapping the result back to the POCO Entities。因此,我们使用两个单独的库:

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

优点

  • 简单性。它少于8行代码。我发现这更容易理解,调试和更改。
  • 更少的代码Slapper.Automapper只需编写几行代码,即使我们有一个复杂的嵌套POCO(即POCO包含List<MyClass1>反过来包含List<MySubClass2>等),Automapper也需要处理您向它扔的任何内容。
  • 速度。这两个库都进行了大量的优化和缓存,以使其运行速度几乎与手动调整的ADO.NET查询一样快。
  • 关注点分离。我们可以将MicroORM更改为另一种,映射仍然有效,反之亦然。
  • 灵活性强Slapper.Automapper处理任意嵌套的层次结构,它不仅限于几个嵌套级别。我们可以轻松进行快速更改,并且一切仍然可以进行。
  • 调试。我们首先可以看到SQL查询运行正常,然后可以检查SQL查询结果是否正确映射回目标POCO实体。
  • 易于SQL开发。我发现创建扁平化查询inner joins以返回扁平化结果要比创建多个select语句(在客户端进行拼接)容易得多。
  • SQL中的优化查询。在高度规范化的数据库中,创建平面查询使SQL引擎可以对整体应用高级优化,如果构造并运行了许多小的单个查询,通常这是不可能的。
  • 信任。Dapper是StackOverflow的后端,而且,Randy Burden有点像超级巨星。我还要说什么吗?
  • 发展速度。我能够执行很多嵌套的异常复杂的查询,并且开发时间很短。
  • 更少的错误。我曾经写过它,但是它确实起作用了,这种技术现在正在帮助为FTSE公司提供动力。几乎没有代码,没有意外行为。

缺点

  • 返回超过1,000,000行的规模。返回<100,000行时效果很好。但是,如果我们要带回> 1,000,000行,为了减少我们和SQL Server之间的通信量,我们不应该使用inner join(将带回来的重复项)弄平它,而应该使用多个select语句并将所有内容缝合在一起客户端(请参阅此页面上的其他答案)。
  • 此技术是面向查询的。我还没有使用这种技术来写数据库,但是我敢肯定Dapper可以完成更多的工作,因为StackOverflow本身使用Dapper作为其数据访问层(DAL)。

性能测试

在我的测试中,Slapper.Automapper在Dapper返回的结果中增加了很小的开销,这意味着它仍然比Entity Framework快10倍,并且组合仍然相当接近SQL + C#能够达到的理论最大速度

在大多数实际情况下,大部分开销将出现在非最佳SQL查询中,而不是在C#端对结果进行一些映射。

性能测试结果

迭代总数:1000

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

工作实例

在此示例中,我们具有的列表Contacts,并且每个列表Contact可以具有一个或多个phone numbers

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表 TestContact

在此处输入图片说明

SQL表 TestPhone

请注意,此表有一个外键ContactID,该外键引用该TestContact表(与List<TestPhone>上面的POCO中的相对应)。

在此处输入图片说明

产生固定结果的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

在此处输入图片说明

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

输出量

在此处输入图片说明

POCO实体层次结构

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

在此处输入图片说明

笔记

Dapper和Slapper.Automapper都在内部缓存所有内容以提高速度。如果遇到内存问题(极不可能),请确保偶尔清除两者的缓存。

确保使用下划线(_)表示法为Slapper.Automapper提供有关如何将结果映射到POCO实体的线索,从而命名返回的列。

确保在每个POCO实体的主键上提供Slapper.Automapper线索(请参阅第几行Slapper.AutoMapper.Configuration.AddIdentifiers)。您也可以Attributes在POCO上使用。如果跳过此步骤,则从理论上讲可能会出错,因为Slapper.Automapper将不知道如何正确执行映射。

更新2015-06-14

成功地将此技术应用于具有40多个标准化表的庞大生产数据库。它的工作完美地与先进的SQL查询超过16映射inner joinleft join到适当的POCO层次(含4层的嵌套)。查询的速度非常快,几乎与在ADO.NET中手动编码的速度一样快(查询通常为52毫秒,从平面结果到POCO层次结构的映射通常为50毫秒)。这确实没有什么革命性的,但是它确实在速度和易用性方面优于Entity Framework,尤其是如果我们正在做的是运行查询。

更新2016-02-19

代码在生产中完美运行了9个月。最新的版本Slapper.Automapper具有我为解决与SQL查询中返回的null有关的问题而应用的所有更改。

更新2017-02-20

代码在生产中完美运行了21个月,并且已经处理了FTSE 250公司中数百名用户的连续查询。

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

更新2019-01-29

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

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


1
我有点不喜欢所有SQL中的表名前缀约定,它不支持Dapper的“ splitOn”之类的东西吗?
tbone

3
Slapper.Automapper需要此表名称约定。是的,Dapper确实支持直接映射到POCO,但是我更喜欢使用Slapper.Automapper,因为代码是如此干净和可维护。
Contango

2
如果您不必为所有列都加上别名,我​​想我会使用Slapper-相反,在您的示例中,我想说的是:,splitOn:“ PhoneId”-那不是很多比为所有内容加上别名更容易?
tbone 2015年

1
我真的很喜欢拍板的外观,只是想知道当一个人没有联系电话时是否尝试过左联接?您有解决这个问题的好方法吗?
不爱

1
@tbone splitOn不包含有关此元素在您的对象中所在位置的任何信息,这就是为什么拍板使用这样的路径的原因
不喜欢

20

我想让它尽可能简单,我的解决方案:

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

我仍然对数据库进行一次调用,虽然现在我执行2个查询而不是一个查询,但是第二个查询使用的是INNER联接,而不是不太理想的LEFT联接。


5
我喜欢这种方法。纯精巧的外观和恕我直言,更易于理解的映射。
阿夫纳

1
似乎可以很容易地将这种扩展方法放到需要几个albmdas的扩展方法中,一个用于键选择器,另一个用于子选择器。类似于.Join(但产生对象图而不是展平结果。
AaronLS

8

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

用法示例

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

此解决方案要注意的一件事是,您的父类负责实例化child属性。 class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } }
Clay

1
这个解决方案非常好,对我们有用。如果没有返回任何子行,我确实必须与children.add一起添加检查以检查是否为null。
tlbignerd

7

根据此答案,Dapper.Net中没有内置的一对多映射支持。查询将始终为每个数据库行返回一个对象。但是,还有一个替代解决方案。


很抱歉,但我不明白如何在查询中使用它?它试图在没有联接的情况下查询数据库2次(并在示例中使用硬编码1)。该示例仅返回1个主要实体,而该主要实体又包含子实体。就我而言,我想投影联接(内部包含列表的列表)。如何使用您提到的链接来做到这一点?在该行显示的链接中:(contact, phones) => { contact.Phones = phones; } 我必须为contactid与contact的contactid相匹配的电话编写过滤器。这是非常低效的。
中医

@Anthony看一下Mike的回答。他使用两个结果集执行一个查询,然后使用Map方法将它们连接起来。当然,您无需根据情况对值进行硬编码。我将在几个小时内整理一个示例。
Damir Arh '02

1
好吧,我终于开始工作了。谢谢!不知道这将对查询数据库的性能造成两倍的影响,而使用单个联接可以完成两倍的性能。
中医

2
我也不明白如果有3张桌子,我需要做些什么更改:p
TCM

1
这完全糟透了..为什么地球上避免加入?
GorillaApe 2012年

2

这是一个粗略的解决方法

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

11
缓存实现存在缺陷。哈希码不是唯一的-两个对象可以具有相同的哈希码。这可能会导致一个对象充满属于其他对象的项目清单..
stmax

2

这是另一种方法:

订单(一个)-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();
}

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.