Bawiłem się tym przez chwilę, ponieważ wydaje się, że wygląda to bardzo podobnie do udokumentowanego przykładu postów / użytkowników , ale jest nieco inny i nie działa dla mnie.
Zakładając następującą uproszczoną konfigurację (kontakt ma wiele numerów telefonów):
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; }
}
Chciałbym skończyć z czymś, co zwraca Contact z wieloma obiektami Phone. W ten sposób, gdybym miał 2 kontakty, każdy z 2 telefonami, mój SQL zwróciłby łączenie tych kontaktów jako zestaw wynikowy z łącznymi 4 wierszami. Następnie Dapper wyskoczyłby 2 obiekty kontaktowe z dwoma telefonami w każdym.
Oto kod SQL w procedurze składowanej:
SELECT *
FROM Contacts
LEFT OUTER JOIN Phones ON Phones.ReferenceId=Contacts.ReferenceId
WHERE clientid=1
Próbowałem tego, ale skończyło się na 4 krotkach (co jest w porządku, ale nie to, na co liczyłem ... oznacza to tylko, że nadal muszę ponownie znormalizować wynik):
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);
a kiedy próbuję innej metody (poniżej), pojawia się wyjątek „Nie można rzutować obiektu typu„ System.Int32 ”na typ„ 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);
Czy ja po prostu robię coś złego? Wygląda to tak, jak przykład postów / właściciela, z tym wyjątkiem, że przechodzę od rodzica do dziecka zamiast z dziecka do rodzica.
Z góry dziękuję
źródło
FYI - otrzymałem odpowiedź Sama, wykonując następujące czynności:
Najpierw dodałem plik klasy o nazwie „Extensions.cs”. Musiałem zmienić słowo kluczowe „to” na „czytelnik” w dwóch miejscach:
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; } } }
Po drugie dodałem następującą metodę, modyfikując ostatni parametr:
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; } }
źródło
Sprawdź https://www.tritac.com/blog/dappernet-by-example/ Możesz zrobić coś takiego:
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;
Mam to z testów dapper.net: https://code.google.com/p/dapper-dot-net/source/browse/Tests/Tests.cs#1343
źródło
Obsługa wielu zestawów wyników
W twoim przypadku znacznie lepiej (i łatwiej) byłoby mieć zapytanie z wieloma wynikami. Oznacza to po prostu, że powinieneś napisać dwie instrukcje Select:
W ten sposób Twoje obiekty byłyby unikalne i nie powielałyby się.
źródło
Oto rozwiązanie wielokrotnego użytku, które jest dość łatwe w użyciu. Jest to niewielka modyfikacja odpowiedzi Andrewsa .
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; }
Przykładowe użycie
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");
źródło
Opierając się na podejściu Sama Saffrona (i Mike'a Gleasona), oto rozwiązanie, które pozwoli na wiele dzieci i wiele poziomów.
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; } } }
Wtedy możesz go odczytać poza funkcją.
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; }
Funkcję map można następnie wywołać ponownie dla następnego obiektu podrzędnego przy użyciu tego samego obiektu nadrzędnego. Można również zaimplementować podziały w instrukcjach odczytu nadrzędnego lub podrzędnego niezależnie od funkcji map.
Oto dodatkowa metoda rozszerzenia „jeden do 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; }
źródło
Gdy zdecydowaliśmy się przenieść naszą DataAccessLayer do procedur składowanych, a procedury te często zwracają wiele połączonych wyników (przykład poniżej).
Cóż, moje podejście jest prawie takie samo, ale może trochę wygodniejsze.
Oto jak może wyglądać Twój kod:
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(); }
Rozbijmy to ...
Rozbudowa:
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 ); } }
Mapper:
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 ); }
To wszystko, ale jeśli chcesz zrobić szybki test, oto modele i procedura dla Ciebie:
Modele:
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(); }
SP:
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
źródło
Chciałem podzielić się moim rozwiązaniem tego problemu i sprawdzić, czy ktoś ma jakieś konstruktywne opinie na temat zastosowanego przeze mnie podejścia?
W projekcie, nad którym pracuję, mam kilka wymagań, które muszę najpierw wyjaśnić:
Tak więc, to, co zrobiłem, to sprawić, by SQL obsłużył hierarchię drugiego - nego poziomu, zwracając pojedynczy ciąg JSON jako kolumnę w oryginalnym wierszu w następujący sposób ( usunięto inne kolumny / właściwości itp., Aby zilustrować ):
Id AttributeJson 4 [{Id:1,Name:"ATT-NAME",Value:"ATT-VALUE-1"}]
Następnie moje POCO są budowane jak poniżej:
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; } }
Gdzie POCO dziedziczy z BaseEntity. (Aby zilustrować, wybrałem dość prostą, jednopoziomową hierarchię, jak pokazano we właściwości „Atrybuty” obiektu klienta).
W mojej warstwie danych mam następującą „klasę danych”, która dziedziczy z POCO
Client
.internal class dataClient : Client { public string AttributeJson { set { Attributes = value.FromJson<List<ClientAttribute>>(); } } }
Jak widać powyżej, dzieje się tak, że SQL zwraca kolumnę o nazwie „AttributeJson”, która jest odwzorowana na właściwość
AttributeJson
w klasie dataClient. Ma to tylko metodę ustawiającą, która deserializuje kod JSON doAttributes
właściwościClient
klasy dziedziczonej . Klasa dataClient znajdujeinternal
się w warstwie dostępu do danych, aClientProvider
(moja fabryka danych) zwraca oryginalny klient POCO do wywołującej aplikacji / biblioteki w następujący sposób:var clients = _conn.Get<dataClient>(); return clients.OfType<Client>().ToList();
Zauważ, że używam Dapper.Contrib i dodałem nową
Get<T>
metodę, która zwraca plikIEnumerable<T>
W przypadku tego rozwiązania należy zwrócić uwagę na kilka rzeczy:
Istnieje oczywisty kompromis wydajności z serializacją JSON - porównałem to z 1050 wierszami z 2
List<T>
właściwościami podrzędnymi , z których każda zawiera 2 encje na liście, i działa przy 279 ms - co jest akceptowalne dla potrzeb moich projektów - dotyczy to również ZERO optymalizacji po stronie SQL, więc powinienem być w stanie zgolić tam kilka ms.Oznacza to, że do zbudowania JSON dla każdej wymaganej
List<T>
właściwości wymagane są dodatkowe zapytania SQL , ale znowu mi to odpowiada, ponieważ znam SQL całkiem dobrze i nie jestem tak biegły w dynamice / odbiciu itp., Więc czuję się, jakbym to zrobił większa kontrola nad rzeczami, bo tak naprawdę rozumiem, co się dzieje pod maską :-)Może być lepsze rozwiązanie niż to, a jeśli tak, to naprawdę byłbym wdzięczny za wysłuchanie twoich przemyśleń - to jest tylko rozwiązanie, które wymyśliłem, które do tej pory pasuje do moich potrzeb w tym projekcie (chociaż jest to eksperymentalne na etapie wysyłania) ).
źródło