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

我写了这个代码来投影一对多的关系,但它不工作:

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>有什么问题。

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

配料:

  • 8行C#。
  • 一些合理简单的SQL,使用一些连接。
  • 两个真棒图书馆。

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

  • 作为微软的Dapper 。
  • 用于映射的Slapper.Automapper 。

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

优点

  • 简单 。 它less于8行的代码。 我发现这样更容易理解,debugging和更改。
  • 代码less 几行代码都是Slapper.Automapper需要处理的东西,即使我们有一个复杂的嵌套POCO(即POCO包含List<MyClass1>List<MySubClass2>等)。
  • 速度 。 这两个库都有非常多的优化和caching,使其运行速度几乎与手动调整的ADO.NET查询一样快。
  • 分离关注 。 我们可以改变一个不同的MicroORM,映射仍然有效,反之亦然。
  • 灵活性 。 Slapper.Automapper处理任意嵌套的层次结构,它不限于嵌套的几个级别。 我们可以很容易地做出迅速的改变,而且一切都会继续。
  • debugging 。 我们可以首先看到SQL查询正常工作,然后我们可以检查SQL查询结果是否正确映射回目标POCO实体。
  • SQL中的开发容易 我发现使用inner joins创build扁平查询返回平坦结果要比创build多个select语句容易得多,并且在客户端进行拼接。
  • 在SQL中优化查询 。 在一个高度规范化的数据库中,创build一个扁平查询允许SQL引擎对整个应用高级优化,如果构build并运行了许多小型的单个查询,这通常是不可能的。
  • 信任 。 Dapper是StackOverflow的后端,而且,Randy Burden也是一位超级巨星。 我需要再说一遍吗?
  • 发展速度。 我能够做一些非常复杂的查询,有很多层次的嵌套,开发时间相当短。
  • 更less的错误。 我曾经写过一次,它刚刚起作用,现在这个技术正在帮助一家富时公司。 代码太less,没有意外的行为。

缺点

  • 缩放超过1,000,000行返回。 返回<100,000行时效果很好。 但是,如果我们要带回大于1,000,000行,为了减less我们与SQL服务器之间的stream量,我们不应该使用inner join (它会带来重复)来压平它,而应该使用多个select语句并将所有内容一起在客户端(见本页的其他答案)。
  • 这种技术是面向查询的 。 我没有使用这种技术来写入数据库,但是我相信Dapper不仅仅能够做更多的额外工作,因为StackOverflow本身使用Dapper作为它的数据访问层(DAL)。

性能testing

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

在大多数实际情况下,大部分的开销都是在一个不太理想的SQL查询中,而不是在C#端进行结果映射。

性能testing结果

总迭代次数:1000

  • Dapper by itself :每个查询1.889毫秒,使用3 lines of code to return the dynamic
  • Dapper + Slapper.Automapper :每个查询Dapper + Slapper.Automapper毫秒,使用额外的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

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

在这里输入图像描述

SQL产生平坦的结果

在我们的SQL查询中,我们使用尽可能多的JOIN语句来获取我们需要的所有数据,并以一种平坦,非规范化的forms 。 是的,这可能会在输出中产生重复,但是当我们使用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; // 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都在内部caching所有的速度。 如果遇到内存问题(非常不可能),请确保您偶尔清除它们的caching。

确保使用下划线( _ )表示法命名返回的列,以便给出关于如何将结果映射到POCO实体的Slapper.Automapper线索。

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

更新2015-06-14

成功地将此技术应用于具有40多个规格化表格的庞大生产数据库。 它完美地将一个高级SQL查询映射到超过16个inner join并将其left join到适当的POCO层次结构(4层嵌套)。 查询的速度非常快,几乎与在ADO.NET中编写代码的速度一样快(查询通常为52毫秒,从平坦结果映射到POCO层次结构通常为50毫秒)。 这实际上没有什么革命性的,但它确实比速度和易用性的Entity Framework,特别是如果我们正在做的是运行查询。

更新2016-02-19

Code已经在生产中运行了9个月。 最新版本的Slapper.Automapper具有我应用于修复与SQL查询中返回的空值有关的问题的所有更改。

更新2017-02-20

代码在生产中已经运行了21个月,并已经处理了来自富时250指数公司数百名用户的连续查询。

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

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

我想保持尽可能简单,我的解决scheme:

 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连接。

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

这是一个粗略的解决方法

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

Andrew的回答略有修改,它利用Funcselect父键而不是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)