Я немного поигрался с этим, потому что кажется, что он очень похож на пример с документированными сообщениями / пользователями , но немного отличается и не работает для меня.
Предполагая следующую упрощенную настройку (у контакта несколько телефонных номеров):
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; }
}
Я бы хотел получить что-то, что возвращает контакт с несколькими объектами Phone. Таким образом, если бы у меня было 2 контакта с 2 телефонами в каждом, мой SQL вернул бы соединение из них в результате набора с 4 полными строками. Затем Dapper выскакивал 2 объекта контактов с двумя телефонами в каждом.
Вот 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);
и когда я пробую другой метод (ниже), я получаю исключение «Невозможно привести объект типа 'System.Int32' к типу 'System.Collections.Generic.IEnumerable`1 [Phone]'».
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);
Я просто что-то не так делаю? Это похоже на пример сообщений / владельца, за исключением того, что я перехожу от родителя к ребенку, а не от ребенка к родителю.
заранее спасибо
источник
К вашему сведению - я получил ответ Сэма, выполнив следующие действия:
Сначала я добавил файл класса под названием «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.net: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343
источник
Поддержка нескольких наборов результатов
В вашем случае было бы намного лучше (и проще) иметь запрос с несколькими наборами результатов. Это просто означает, что вы должны написать два оператора выбора:
Таким образом, ваши объекты будут уникальными и не будут дублироваться.
источник
Вот решение многоразового использования, которое довольно просто использовать. Это небольшая модификация ответа Эндрюса .
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");
источник
Основанное на подходе Сэма Сафрона (и Майка Глисона), вот решение, которое позволит использовать несколько детей и несколько уровней.
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; }
Затем функция карты может быть вызвана снова для следующего дочернего объекта, используя тот же родительский объект. Вы также можете реализовать разбиение в родительских или дочерних операторах чтения независимо от функции карты.
Вот дополнительный метод расширения от одного до N
public static TFirst MapChildren<TFirst, TSecond, TKey> ( this SqlMapper.GridReader reader, TFirst parent, IEnumerable<TSecond> children, Func<TFirst, TKey> firstKey, Func<TSecond, TKey> secondKey, Action<TFirst, IEnumerable<TSecond>> addChildren ) { if (parent == null || children == null || !children.Any()) { return parent; } Dictionary<TKey, IEnumerable<TSecond>> childMap = children .GroupBy(secondKey) .ToDictionary(g => g.Key, g => g.AsEnumerable()); if (childMap.TryGetValue(firstKey(parent), out IEnumerable<TSecond> foundChildren)) { addChildren(parent, foundChildren); } return parent; }
источник
Однажды мы решили переместить наш DataAccessLayer в хранимые процедуры, и эти процедуры часто возвращают несколько связанных результатов (пример ниже).
Что ж, мой подход почти такой же, но, может быть, немного удобнее.
Вот как может выглядеть ваш код:
using ( var conn = GetConn() ) { var res = await conn .StoredProc<Person>( procName, procParams ) .Include<Book>( ( p, b ) => p.Books = b.Where( x => x.PersonId == p.Id ).ToList() ) .Include<Course>( ( p, c ) => p.Courses = c.Where( x => x.PersonId == p.Id ).ToList() ) .Include<Course, Mark>( ( c, m ) => c.Marks = m.Where( x => x.CourseId == c.Id ).ToList() ) .Execute(); }
Давайте разберемся ...
Расширение:
public static class SqlExtensions { public static StoredProcMapper<T> StoredProc<T>( this SqlConnection conn, string procName, object procParams ) { return StoredProcMapper<T> .Create( conn ) .Call( procName, procParams ); } }
Картограф:
public class StoredProcMapper<T> { public static StoredProcMapper<T> Create( SqlConnection conn ) { return new StoredProcMapper<T>( conn ); } private List<MergeInfo> _merges = new List<MergeInfo>(); public SqlConnection Connection { get; } public string ProcName { get; private set; } public object Parameters { get; private set; } private StoredProcMapper( SqlConnection conn ) { Connection = conn; _merges.Add( new MergeInfo( typeof( T ) ) ); } public StoredProcMapper<T> Call( object procName, object parameters ) { ProcName = procName.ToString(); Parameters = parameters; return this; } public StoredProcMapper<T> Include<TChild>( MergeDelegate<T, TChild> mapper ) { return Include<T, TChild>( mapper ); } public StoredProcMapper<T> Include<TParent, TChild>( MergeDelegate<TParent, TChild> mapper ) { _merges.Add( new MergeInfo<TParent, TChild>( mapper ) ); return this; } public async Task<List<T>> Execute() { if ( string.IsNullOrEmpty( ProcName ) ) throw new Exception( $"Procedure name not specified! Please use '{nameof(Call)}' method before '{nameof( Execute )}'" ); var gridReader = await Connection.QueryMultipleAsync( ProcName, Parameters, commandType: CommandType.StoredProcedure ); foreach ( var merge in _merges ) { merge.Result = gridReader .Read( merge.Type ) .ToList(); } foreach ( var merge in _merges ) { if ( merge.ParentType == null ) continue; var parentMerge = _merges.FirstOrDefault( x => x.Type == merge.ParentType ); if ( parentMerge == null ) throw new Exception( $"Wrong parent type '{merge.ParentType.FullName}' for type '{merge.Type.FullName}'." ); foreach ( var parent in parentMerge.Result ) { merge.Merge( parent, merge.Result ); } } return _merges .First() .Result .Cast<T>() .ToList(); } private class MergeInfo { public Type Type { get; } public Type ParentType { get; } public IEnumerable Result { get; set; } public MergeInfo( Type type, Type parentType = null ) { Type = type; ParentType = parentType; } public void Merge( object parent, IEnumerable children ) { MergeInternal( parent, children ); } public virtual void MergeInternal( object parent, IEnumerable children ) { } } private class MergeInfo<TParent, TChild> : MergeInfo { public MergeDelegate<TParent, TChild> Action { get; } public MergeInfo( MergeDelegate<TParent, TChild> mergeAction ) : base( typeof( TChild ), typeof( TParent ) ) { Action = mergeAction; } public override void MergeInternal( object parent, IEnumerable children ) { Action( (TParent)parent, children.Cast<TChild>() ); } } public delegate void MergeDelegate<TParent, TChild>( TParent parent, IEnumerable<TChild> children ); }
Это все, но если вы хотите провести быстрый тест, вот модели и процедуры для вас:
Модели:
public class Person { public Guid Id { get; set; } public string Name { get; set; } public List<Course> Courses { get; set; } public List<Book> Books { get; set; } public override string ToString() => Name; } public class Book { public Guid Id { get; set; } public Guid PersonId { get; set; } public string Name { get; set; } public override string ToString() => Name; } public class Course { public Guid Id { get; set; } public Guid PersonId { get; set; } public string Name { get; set; } public List<Mark> Marks { get; set; } public override string ToString() => Name; } public class Mark { public Guid Id { get; set; } public Guid CourseId { get; set; } public int Value { get; set; } public override string ToString() => Value.ToString(); }
СП:
if exists ( select * from sysobjects where id = object_id(N'dbo.MultiTest') and ObjectProperty( id, N'IsProcedure' ) = 1 ) begin drop procedure dbo.MultiTest end go create procedure dbo.MultiTest @PersonId UniqueIdentifier as begin declare @tmpPersons table ( Id UniqueIdentifier, Name nvarchar(50) ); declare @tmpBooks table ( Id UniqueIdentifier, PersonId UniqueIdentifier, Name nvarchar(50) ) declare @tmpCourses table ( Id UniqueIdentifier, PersonId UniqueIdentifier, Name nvarchar(50) ) declare @tmpMarks table ( Id UniqueIdentifier, CourseId UniqueIdentifier, Value int ) -------------------------------------------------- insert into @tmpPersons values ( '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Иван' ), ( '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Василий' ), ( '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Алефтина' ) insert into @tmpBooks values ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Математика' ), ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Физика' ), ( NewId(), '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Книга Геометрия' ), ( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Биология' ), ( NewId(), '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Книга Химия' ), ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга История' ), ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Литература' ), ( NewId(), '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Книга Древне-шумерский диалект иврита' ) insert into @tmpCourses values ( '30945b68-a6ef-4da8-9a35-d3b2845e7de3', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Математика' ), ( '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Физика' ), ( '92bbefd1-9fec-4dc7-bb58-986eadb105c8', '576fb8e8-41a2-43a9-8e77-a8213aa6e387', N'Геометрия' ), ( '923a2f0c-c5c7-4394-847c-c5028fe14711', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Биология' ), ( 'ace50388-eb05-4c46-82a9-5836cf0c988c', '467953a5-cb5f-4d06-9fad-505b3bba2058', N'Химия' ), ( '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'История' ), ( '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Литература' ), ( '73ac366d-c7c2-4480-9513-28c17967db1a', '52a719bf-6f1f-48ac-9e1f-4532cfc70d96', N'Древне-шумерский диалект иврита' ) insert into @tmpMarks values ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 98 ), ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 87 ), ( NewId(), '30945b68-a6ef-4da8-9a35-d3b2845e7de3', 76 ), ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 89 ), ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 78 ), ( NewId(), '7881f090-ccd6-4fb9-a1e0-ff4ff5c18450', 67 ), ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 79 ), ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 68 ), ( NewId(), '92bbefd1-9fec-4dc7-bb58-986eadb105c8', 75 ), ---------- ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 198 ), ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 187 ), ( NewId(), '923a2f0c-c5c7-4394-847c-c5028fe14711', 176 ), ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 189 ), ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 178 ), ( NewId(), 'ace50388-eb05-4c46-82a9-5836cf0c988c', 167 ), ---------- ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 8 ), ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 7 ), ( NewId(), '53ea69fb-6cc4-4a6f-82c2-0afbaa8cb410', 6 ), ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 9 ), ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 8 ), ( NewId(), '7290c5f7-1000-4f44-a5f0-6a7cf8a8efab', 7 ), ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 9 ), ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 8 ), ( NewId(), '73ac366d-c7c2-4480-9513-28c17967db1a', 5 ) -------------------------------------------------- select * from @tmpPersons select * from @tmpBooks select * from @tmpCourses select * from @tmpMarks end go
источник
Я хотел поделиться своим решением этой проблемы и посмотреть, есть ли у кого-нибудь конструктивные отзывы о подходе, который я использовал?
В проекте, над которым я работаю, у меня есть несколько требований, которые мне нужно объяснить в первую очередь:
Итак, что я сделал, чтобы получить SQL для обработки 2 - го - го уровня, возвращая иерархию строки Single JSON в качестве столбца в исходной строке следующим образом ( раздели другие столбцы / свойства и т.д. , чтобы проиллюстрировать ):
Id AttributeJson 4 [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}]
Затем мои POCO создаются, как показано ниже:
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 наследуются от BaseEntity. (Для иллюстрации я выбрал довольно простую одноуровневую иерархию, как показано свойством «Attributes» клиентского объекта.)
Затем на моем уровне данных имеется следующий «класс данных», который наследуется от POCO
Client
.internal class dataClient : Client { public string AttributeJson { set { Attributes = value.FromJson<List<ClientAttribute>>(); } } }
Как вы можете видеть выше, происходит то, что SQL возвращает столбец с именем «AttributeJson», который сопоставлен со свойством
AttributeJson
в классе dataClient. У него есть только сеттер, который десериализует JSON вAttributes
свойство унаследованногоClient
класса. Класс dataClient относитсяinternal
к уровню доступа к данным, аClientProvider
(моя фабрика данных) возвращает исходный клиентский POCO в вызывающее приложение / библиотеку следующим образом:var clients = _conn.Get<dataClient>(); return clients.OfType<Client>().ToList();
Обратите внимание, что я использую Dapper.Contrib и добавил новый
Get<T>
метод, который возвращаетIEnumerable<T>
При использовании этого решения необходимо отметить несколько моментов:
Существует очевидный компромисс производительности с сериализацией JSON - я сравнил это с 1050 строками с 2 вспомогательными
List<T>
свойствами, каждая с 2 объектами в списке, и он работает на частоте 279 мс - что приемлемо для потребностей моих проектов - это также с НУЛЕВАЯ оптимизация на стороне SQL, поэтому я смогу сэкономить несколько мс.Это означает, что для создания JSON для каждого необходимого
List<T>
свойства требуются дополнительные SQL-запросы , но опять же, это меня устраивает, так как я довольно хорошо знаю SQL и не очень хорошо разбираюсь в динамике / отражении и т. Д., Поэтому я чувствую, что у меня есть больше контроля над вещами, поскольку я действительно понимаю, что происходит под капотом :-)Вполне может быть лучшее решение, чем это, и если оно есть, я был бы очень признателен, если бы выслушал ваши мысли - это как раз то решение, которое я придумал, которое пока соответствует моим потребностям для этого проекта (хотя это экспериментальное решение на этапе публикации. ).
источник