【问题标题】:How do I write one to many query in Dapper.Net?如何在 Dapper.Net 中编写一对多查询?
【发布时间】:2012-02-19 15:27:21
【问题描述】:

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

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

我摆脱了异常!但是,Employees 根本没有映射。我仍然不确定IEnumerable&lt;Employee&gt; 在第一次查询中有什么问题。

【问题讨论】:

  • 您的实体是什么样的?
  • 怎么不工作?你有例外吗?出乎意料的结果?
  • 这个错误没有意义,这就是我没有费心发布它的原因。我得到:“{”值不能为空。\r\n参数名称:con“}”。在 SqlMapper 中引发错误的行是:" il.Emit(OpCodes.Newobj, type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null));"

标签: c# .net dapper


【解决方案1】:

这篇文章展示了如何查询highly normalised SQL database,并将结果映射到一组高度嵌套的 C# POCO 对象。

成分:

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

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

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

优势

  • 简单。它的代码不到 8 行。我发现这更容易理解、调试和更改。
  • 更少的代码。即使我们有一个复杂的嵌套 POCO(即 POCO 包含 List&lt;MyClass1&gt; 反过来又包含 List&lt;MySubClass2&gt; 等),几行代码就是 Slapper.Automapper 需要处理的所有内容。
  • 速度。这两个库都具有大量的优化和缓存,使其运行速度几乎与手动调整的 ADO.NET 查询一样快。
  • 关注点分离。我们可以将 MicroORM 更改为不同的,映射仍然有效,反之亦然。
  • 灵活性Slapper.Automapper 处理任意嵌套的层次结构,它不限于几层嵌套。我们可以轻松地进行快速更改,并且一切正常。
  • 调试。我们首先可以看到 SQL 查询工作正常,然后我们可以检查 SQL 查询结果是否正确映射回目标 POCO 实体。
  • 易于使用 SQL 进行开发。我发现使用 inner joins 创建扁平化查询以返回扁平化结果比创建多个选择语句(在客户端进行拼接)要容易得多。
  • SQL 中的优化查询。在高度规范化的数据库中,创建平面查询允许 SQL 引擎对整体应用高级优化,如果构建和运行许多小的单个查询,这通常是不可能的。
  • 信任。 Dapper 是 StackOverflow 的后端,而且,Randy Burden 有点像超级明星。还需要我说吗?
  • 开发速度。我能够做一些非常复杂的查询,有很多层次的嵌套,而且开发时间很短。
  • 更少的错误。我写过一次,它很有效,现在这项技术正在帮助一家 FTSE 公司。代码太少,不会出现意外行为。

缺点

  • 返回超过 1,000,000 行。在返回 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 表(这对应于上面POCO 中的List&lt;TestPhone&gt;)。

产生平面结果的 SQL

在我们的 SQL 查询中,我们使用尽可能多的 JOIN 语句来获取我们需要的所有数据,在 flat, denormalized form 中。是的,这可能会在输出中产生重复,但是当我们使用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&lt;TestContact&gt;,每个 TestContact 都有一个 List&lt;TestPhone&gt;

注意事项

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

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

确保为每个 POCO 实体的主键提供 Slapper.Automapper 线索(请参阅行 Slapper.AutoMapper.Configuration.AddIdentifiers)。为此,您还可以在 POCO 上使用Attributes。如果你跳过这一步,那么它可能会出错(理论上),因为 Slapper.Automapper 不知道如何正确地进行映射。

2015-06-14 更新

成功地将这项技术应用于一个包含 40 多个规范化表的大型生产数据库。它完美地将具有超过 16 个 inner joinleft join 的高级 SQL 查询映射到适当的 POCO 层次结构(具有 4 级嵌套)。查询速度非常快,几乎与在 ADO.NET 中手动编码一样快(查询通常需要 52 毫秒,从平面结果映射到 POCO 层次结构需要 50 毫秒)。这确实不是什么革命性的东西,但它在速度和易用性方面确实胜过 Entity Framework,尤其是当我们所做的只是运行查询时。

2016 年 2 月 19 日更新

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

2017 年 2 月 20 日更新

代码已在生产环境中完美运行 21 个月,并已处理来自 FTSE 250 公司数百名用户的持续查询。

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

2019-01-29 更新

小幅更新以添加更多代码 cmets。

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

【讨论】:

  • 我真的不喜欢你所有 sql 中的表名前缀约定,它不支持 Dapper 的“splitOn”之类的东西?
  • Slapper.Automapper 需要此表名称约定。是的,Dapper 确实支持直接映射到 POCO,但我更喜欢使用 Slapper.Automapper,因为代码非常干净且可维护。
  • 如果您不必为所有列设置别名,我想我会使用 Slapper - 相反,在您的示例中,我想说: , splitOn: "PhoneId" -这不是比给所有东西都取别名更容易吗?
  • 我真的很喜欢 slapper 的样子,只是想知道当一个人没有联系电话时你是否尝试过左连接?你有什么好的处理方法吗?
  • @tbone splitOn 不包含有关此元素在对象中的位置的任何信息,这就是为什么 slapper 使用这样的路径
【解决方案2】:

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

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 个查询而不是 1 个,但第二个查询使用的是 INNER 联接而不是不太理想的 LEFT 联接。

【讨论】:

  • 我喜欢这种方法。纯粹的 dapper 和恕我直言更容易理解的映射。
  • 似乎这很容易放入一个扩展方法中,该方法需要几个 albmda,一个用于键选择器,一个用于子选择器。类似于.Join(,但生成一个对象图而不是扁平化结果。
【解决方案3】:

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

【讨论】:

  • 很抱歉,我不明白如何在查询中使用它?它试图在没有连接的情况下查询数据库 2 次(并在示例中使用硬编码 1)。该示例仅返回 1 个主要实体,而该主要实体又包含子实体。就我而言,我想投影加入(内部包含列表的列表)。我如何使用您提到的链接来做到这一点?在该行显示的链接中:(contact, phones) =&gt; { contact.Phones = phones; } 我必须为联系人 ID 与联系人的联系人 ID 匹配的电话编写一个过滤器。这是相当低效的。
  • @Anthony 看看 Mike 的回答。他使用两个结果集执行单个查询,然后使用 Map 方法将它们连接起来。当然,您不需要对您的案例中的值进行硬编码。我会在几个小时内尝试整理一个示例。
  • 好吧,我终于搞定了。谢谢!不知道这将如何影响查询数据库的性能 2 倍于使用单个连接所能实现的性能。
  • 另外我不明白如果有 3 个表我需要做哪些改变:p
  • 这太糟糕了..为什么要避免加入?
【解决方案4】:

对 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)

【讨论】:

  • 此解决方案需要注意的一点是,您的父类负责实例化子属性。 class Parent { public List&lt;Child&gt; Children { get; set; } public Parent() { this.Children = new List&lt;Child&gt;(); } }
  • 这个解决方案非常好,对我们有用。如果没有返回子行,我确实必须添加与 children.add 的检查以检查 null。
  • 这很有趣...如果父类没有实例化Children,那么Children 将始终为空,即使您通过children 变量在内部实例化它也是如此。 children 应该是对 Parent.Children 的引用,因此对 children 所做的任何更改都会影响 Parent.Children 还是我遗漏了什么?
【解决方案5】:

这是一个粗略的解决方法

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

【讨论】:

  • 缓存实现存在缺陷。哈希码不是唯一的——两个对象可以有相同的哈希码。这可能会导致对象列表被属于另一个对象的项目填充..
【解决方案6】:

这是另一种方法:

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

【讨论】:

    猜你喜欢
    • 2020-01-15
    • 2018-10-07
    • 1970-01-01
    • 2011-04-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多