Multi-Mapper创build对象层次结构

我一直在玩这个有点,因为它看起来很像logging的post/用户的例子 ,但它略有不同,并没有为我工作。

假设以下简化设置(联系人有多个电话号码):

public class Contact { public int ContactID { get; set; } public string ContactName { get; set; } public IEnumerable<Phone> Phones { get; set; } } public class Phone { public int PhoneId { get; set; } public int ContactID { get; set; } // foreign key public string Number { get; set; } public string Type { get; set; } public bool IsActive { get; set; } } 

我很想结束与多个电话对象返回联系人的东西。 这样,如果我有2个联系人,每个有2个电话,那么我的SQL将返回一个包含总共4行的结果集的联接。 然后Dapper会用两个电话分别popup两个联系对象。

这里是存储过程中的SQL:

 SELECT * FROM Contacts LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId WHERE clientid=1 

我试过这个,但最后得到了4个元组(这是可以的,但不是我所希望的…这只是意味着我仍然需要重新规范化结果):

 var x = cn.Query<Contact, Phone, Tuple<Contact, Phone>>("sproc_Contacts_SelectByClient", (co, ph) => Tuple.Create(co, ph), splitOn: "PhoneId", param: p, commandType: CommandType.StoredProcedure); 

当我尝试另一种方法(下面)时,我得到一个exception“无法投入types'System.Int32'的对象键入'System.Collections.Generic.IEnumerable`1 [电话]'。

 var x = cn.Query<Contact, IEnumerable<Phone>, Contact>("sproc_Contacts_SelectByClient", (co, ph) => { co.Phones = ph; return co; }, splitOn: "PhoneId", param: p, commandType: CommandType.StoredProcedure); 

我只是做错了什么? 这看起来就像post/所有者的例子,除了我从父母到孩子,而不是孩子父母。

提前致谢

你没有做错什么,它不是API的devise方式。 所有Query API将始终为每个数据库行返回一个对象。

所以,这个方法在多个方向上运行良好,但对于一个多个多图却不太好。

这里有两个问题:

  1. 如果我们引入一个与查询一起工作的内置映射器,我们将会“丢弃”重复的数据。 (联系人。*在您的查询中重复)

  2. 如果我们把它devise成与一对多的工作,我们将需要某种forms的身份地图。 这增加了复杂性。


举个例子,这个查询是有效的,如果你只需要拉less量的logging,如果你把这个数以百万计的东西变得更加棘手,因为你需要stream和不能加载所有内容:

 var sql = "set nocount on DECLARE @t TABLE(ContactID int, ContactName nvarchar(100)) INSERT @t SELECT * FROM Contacts WHERE clientid=1 set nocount off SELECT * FROM @t SELECT * FROM Phone where ContactId in (select t.ContactId from @tt)" 

你可以做的是扩展GridReader以允许重新映射:

 var mapped = cnn.QueryMultiple(sql) .Map<Contact,Phone, int> ( contact => contact.ContactID, phone => phone.ContactID, (contact, phones) => { contact.Phones = phones }; ); 

假设你扩展你的GridReader和一个映射器:

 public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey> ( this GridReader reader, Func<TFirst, TKey> firstKey, Func<TSecond, TKey> secondKey, Action<TFirst, IEnumerable<TSecond>> addChildren ) { var first = reader.Read<TFirst>().ToList(); var childMap = reader .Read<TSecond>() .GroupBy(s => secondKey(s)) .ToDictionary(g => g.Key, g => g.AsEnumerable()); foreach (var item in first) { IEnumerable<TSecond> children; if(childMap.TryGetValue(firstKey(item), out children)) { addChildren(item,children); } } return first; } 

由于这有点棘手和复杂,有警告。 我并不倾向于将其纳入核心。

供参考 – 我得到山姆的答案工作做到以下几点:

首先,我添加了一个名为“Extensions.cs”的类文件。 我不得不在两个地方把“this”这个关键字改成“reader”:

 using System; using System.Collections.Generic; using System.Linq; using Dapper; namespace TestMySQL.Helpers { public static class Extensions { public static IEnumerable<TFirst> Map<TFirst, TSecond, TKey> ( this Dapper.SqlMapper.GridReader reader, Func<TFirst, TKey> firstKey, Func<TSecond, TKey> secondKey, Action<TFirst, IEnumerable<TSecond>> addChildren ) { var first = reader.Read<TFirst>().ToList(); var childMap = reader .Read<TSecond>() .GroupBy(s => secondKey(s)) .ToDictionary(g => g.Key, g => g.AsEnumerable()); foreach (var item in first) { IEnumerable<TSecond> children; if (childMap.TryGetValue(firstKey(item), out children)) { addChildren(item, children); } } return first; } } } 

其次,我添加了下面的方法,修改最后一个参数:

 public IEnumerable<Contact> GetContactsAndPhoneNumbers() { var sql = @" SELECT * FROM Contacts WHERE clientid=1 SELECT * FROM Phone where ContactId in (select ContactId FROM Contacts WHERE clientid=1)"; using (var connection = GetOpenConnection()) { var mapped = connection.QueryMultiple(sql) .Map<Contact,Phone, int> ( contact => contact.ContactID, phone => phone.ContactID, (contact, phones) => { contact.Phones = phones; } ); return mapped; } } 

看看https://www.tritac.com/blog/dappernet-by-example/你可以做这样的事情:;

 public class Shop { public int? Id {get;set;} public string Name {get;set;} public string Url {get;set;} public IList<Account> Accounts {get;set;} } public class Account { public int? Id {get;set;} public string Name {get;set;} public string Address {get;set;} public string Country {get;set;} public int ShopId {get;set;} } var lookup = new Dictionary<int, Shop>() conn.Query<Shop, Account, Shop>(@" SELECT s.*, a.* FROM Shop s INNER JOIN Account a ON s.ShopId = a.ShopId ", (s, a) => { Shop shop; if (!lookup.TryGetValue(s.Id, out shop)) { lookup.Add(s.Id, shop = s); } shop.Accounts.Add(a); return shop; }, ).AsQueryable(); var resultList = lookup.Values; 

我从dapper.nettesting中得到了这个: https : //code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343

多结果集支持

在你的情况下,会有更好的(也更容易)有一个多结果集查询。 这只是意味着你应该写两个select语句:

  1. 一个返回联系人
  2. 还有一个返回他们的电话号码

这样你的对象将是唯一的,不会重复。

这是一个很容易使用的可重复使用的解决scheme。 这是安德鲁斯答案的一个微小的修改。

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

用法示例

 public class Contact { public int ContactID { get; set; } public string ContactName { get; set; } public List<Phone> Phones { get; set; } // must be IList public Contact() { this.Phones = new List<Phone>(); // POCO is responsible for instantiating child list } } public class Phone { public int PhoneID { get; set; } public int ContactID { get; set; } // foreign key public string Number { get; set; } public string Type { get; set; } public bool IsActive { get; set; } } conn.QueryParentChild<Contact, Phone, int>( "SELECT * FROM Contact LEFT OUTER JOIN Phone ON Contact.ContactID = Phone.ContactID", contact => contact.ContactID, contact => contact.Phones, splitOn: "PhoneId"); 

基于Sam Saffron(和Mike Gleason)的方法,这是一个解决scheme,将允许多个孩子和多个级别。

 using System; using System.Collections.Generic; using System.Linq; using Dapper; namespace TestMySQL.Helpers { public static class Extensions { public static IEnumerable<TFirst> MapChild<TFirst, TSecond, TKey> ( this SqlMapper.GridReader reader, List<TFirst> parent, List<TSecond> child, Func<TFirst, TKey> firstKey, Func<TSecond, TKey> secondKey, Action<TFirst, IEnumerable<TSecond>> addChildren ) { var childMap = child .GroupBy(secondKey) .ToDictionary(g => g.Key, g => g.AsEnumerable()); foreach (var item in parent) { IEnumerable<TSecond> children; if (childMap.TryGetValue(firstKey(item), out children)) { addChildren(item, children); } } return parent; } } } 

然后你可以在函数之外读取它。

 using (var multi = conn.QueryMultiple(sql)) { var contactList = multi.Read<Contact>().ToList(); var phoneList = multi.Read<Phone>().ToList; contactList = multi.MapChild ( contactList, phoneList, contact => contact.Id, phone => phone.ContactId, (contact, phone) => {contact.Phone = phone;} ).ToList(); return contactList; } 

然后可以使用相同的父对象为下一个子对象再次调用map函数。 您也可以独立于地图function在父或子读取语句上实施拆分 。

我想分享我的解决scheme,看看有没有人对我使用的方法有任何build设性的反馈?

在我正在进行的项目中,我有一些要求,首先需要解释:

  1. 我必须尽可能保持POCO的清洁,因为这些类将在API包装器中公开共享。
  2. 由于上述要求,我的POCO在一个单独的类库中
  3. 将会有多个对象层次结构级别会根据数据而变化(所以我不能使用genericstypes映射器,或者我不得不编写大量的对象来迎合所有可能的事件)

所以,我所做的就是通过返回一个单JSONstring作为原始行的一列来处理第二 – 第n级的层次结构,如下所示( 剥离出其他的列/属性等来说明 ):

 Id AttributeJson 4 [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}] 

那么,我的POCO的build立如下:

 public abstract class BaseEntity { [KeyAttribute] public int Id { get; set; } } public class Client : BaseEntity { public List<ClientAttribute> Attributes{ get; set; } } public class ClientAttribute : BaseEntity { public string Name { get; set; } public string Value { get; set; } } 

POCO从BaseEntityinheritance。 (为了说明,我已经select了一个相当简单的单层次层次结构,如客户端对象的“属性”属性所示)。

然后在我的数据层中有从POCO Clientinheritance的以下“数据类”。

 internal class dataClient : Client { public string AttributeJson { set { Attributes = value.FromJson<List<ClientAttribute>>(); } } } 

正如你上面所看到的,SQL发生的是返回一个名为“AttributeJson”的列,该列被映射到dataClient类中的属性AttributeJson 。 这只有一个将JSON反序列化到inheritance的Client类的Attributes属性的setter。 dataClient类是数据访问层的internalClientProvider (我的数据工厂)将原始的Client POCO返回给调用的App / Library,如下所示:

 var clients = _conn.Get<dataClient>(); return clients.OfType<Client>().ToList(); 

请注意,我正在使用Dapper.Contrib并添加了一个新的Get<T>方法,该方法返回一个IEnumerable<T>

这个解决scheme有几点需要注意:

  1. 有一个明显的性能与JSON序列化交易 – 我已经用10个行与2个List<T>属性进行了基准testing,每个属性List<T>有2个实体,它的时钟速度为279ms,这对我的项目需求是可以接受的 -这也是在SQL方面的零优化,所以我应该能够在那里几个毫秒。

  2. 这意味着需要额外的SQL查询来build立每个必需的List<T>属性的JSON,但是这又适合我,因为我知道SQL很好,而且在dynamic/reflection等方面也不那么stream利。所以这样我觉得我有更多的控制权,因为我真的明白发生了什么事情下:-)

或许有更好的解决scheme,如果有的话,我会非常感激听到你的想法 – 这只是我提出的解决scheme,到目前为止,这符合我对这个项目的需求(虽然这是在发布阶段的实验)。

Interesting Posts