Napisałem ten kod, aby zaprojektować relację jeden do wielu, ale nie działa:
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);
}
}
Czy ktoś może dostrzec błąd?
EDYTOWAĆ:
Oto moje podmioty:
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>();
}
}
EDYTOWAĆ:
Zmieniam zapytanie na:
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");
i pozbywam się wyjątków! Jednak pracownicy nie są w ogóle mapowani. Nadal nie jestem pewien, z jakim problemem miał IEnumerable<Employee>
w pierwszym zapytaniu.
Odpowiedzi:
W tym poście pokazano, jak wysłać zapytanie do wysoce znormalizowanej bazy danych SQL i zmapować wynik na zestaw wysoce zagnieżdżonych obiektów C # POCO.
Składniki:
Spostrzeżenie, które pozwoliło mi rozwiązać ten problem, polega na oddzieleniu
MicroORM
odmapping the result back to the POCO Entities
. Dlatego używamy dwóch oddzielnych bibliotek:Zasadniczo, używamy Dapper do przeszukiwania bazy danych, a następnie użyć Slapper.Automapper mapować wynik prosto do naszych Poços.
Zalety
List<MyClass1>
co z kolei zawieraList<MySubClass2>
, itp.).inner joins
celu zwrócenia płaskich wyników jest znacznie łatwiejsze niż tworzenie wielu instrukcji wyboru ze zszywaniem po stronie klienta.Niedogodności
inner join
(co przywraca duplikaty), zamiast tego powinniśmy użyć wieluselect
instrukcji i zszyć wszystko z powrotem w po stronie klienta (zobacz inne odpowiedzi na tej stronie).Test wydajności
W moich testach Slapper.Automapper dodał niewielki narzut do wyników zwracanych przez Dapper, co oznaczało, że był nadal 10x szybszy niż Entity Framework, a kombinacja jest nadal bardzo bliska teoretycznej maksymalnej szybkości , do jakiej jest zdolny SQL + C # .
W większości praktycznych przypadków większość narzutu wiązałaby się z mniej optymalnym zapytaniem SQL, a nie z pewnym mapowaniem wyników po stronie C #.
Wyniki testów wydajności
Całkowita liczba iteracji: 1000
Dapper by itself
: 1,889 milisekund na zapytanie przy użyciu3 lines of code to return the dynamic
.Dapper + Slapper.Automapper
: 2,463 milisekund na zapytanie przy użyciu dodatkowego3 lines of code for the query + mapping from dynamic to POCO Entities
.Przykład praktyczny
W tym przykładzie mamy listę
Contacts
i każdyContact
może mieć jeden lub więcejphone numbers
.Podmioty 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; } }
Tabela SQL
TestContact
Tabela SQL
TestPhone
Zauważ, że ta tabela ma klucz obcy,
ContactID
który odwołuje się doTestContact
tabeli (odpowiada toList<TestPhone>
w powyższym POCO).SQL, który daje płaski wynik
W naszym zapytaniu SQL używamy tylu
JOIN
instrukcji, ile potrzebujemy, aby uzyskać wszystkie potrzebne dane w płaskiej, zdenormalizowanej formie . Tak, może to spowodować powstanie duplikatów w wyniku, ale te duplikaty zostaną automatycznie wyeliminowane, gdy użyjemy Slapper.Automapper do automatycznego mapowania wyniku tego zapytania bezpośrednio na naszą mapę obiektów 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
Kod 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); } } } }
Wynik
Hierarchia jednostek POCO
Patrząc w Visual Studio, widzimy, że Slapper.Automapper poprawnie zapełnił nasze encje POCO, tj. Mamy a
List<TestContact>
, a każdyTestContact
maList<TestPhone>
.Uwagi
Zarówno Dapper, jak i Slapper.Automapper buforuje wszystko wewnętrznie w celu zwiększenia szybkości. Jeśli napotkasz problemy z pamięcią (bardzo mało prawdopodobne), od czasu do czasu wyczyść pamięć podręczną dla obu z nich.
Upewnij się, że nazwałeś wracające kolumny, używając notacji podkreślenia (
_
), aby podać Slapper.Automapper wskazówki, jak mapować wynik na jednostki POCO.Upewnij się, że podajesz wskazówki Slapper.Automapper dotyczące klucza podstawowego dla każdej jednostki POCO (patrz wiersze
Slapper.AutoMapper.Configuration.AddIdentifiers
). Możesz również użyćAttributes
do tego w POCO. Jeśli pominiesz ten krok, może pójść źle (w teorii), ponieważ Slapper.Automapper nie wiedziałby, jak poprawnie wykonać mapowanie.Aktualizacja 2015-06-14
Z powodzeniem zastosowaliśmy tę technikę w ogromnej produkcyjnej bazie danych z ponad 40 znormalizowanymi tabelami. Doskonale działało przy mapowaniu zaawansowanego zapytania SQL z ponad 16
inner join
ileft join
we właściwej hierarchii POCO (z 4 poziomami zagnieżdżenia). Zapytania są oszałamiająco szybkie, prawie tak szybkie, jak ręczne kodowanie ich w ADO.NET (zazwyczaj było to 52 milisekundy dla zapytania i 50 milisekund dla mapowania z płaskiego wyniku do hierarchii POCO). To naprawdę nic rewolucyjnego, ale z pewnością przewyższa Entity Framework pod względem szybkości i łatwości użycia, zwłaszcza jeśli wszystko, co robimy, to uruchamianie zapytań.Aktualizacja 2016-02-19
Kod działał bezbłędnie w produkcji od 9 miesięcy. Najnowsza wersja
Slapper.Automapper
zawiera wszystkie zmiany, które zastosowałem, aby rozwiązać problem związany z zwracaniem wartości null w zapytaniu SQL.Aktualizacja 2017-02-20
Kod działał bezproblemowo w produkcji przez 21 miesięcy i obsługiwał ciągłe zapytania setek użytkowników w firmie FTSE 250.
Slapper.Automapper
świetnie nadaje się również do mapowania pliku .csv bezpośrednio na listę POCO. Wczytaj plik .csv do listy IDictionary, a następnie zamapuj go bezpośrednio na listę docelową POCO. Jedyną sztuczką jest to, że musisz dodać właściwośćint Id {get; set}
i upewnić się, że jest unikalna dla każdego wiersza (w przeciwnym razie automapper nie będzie w stanie rozróżnić wierszy).Aktualizacja 2019-01-29
Niewielka aktualizacja, aby dodać więcej komentarzy do kodu.
Zobacz: https://github.com/SlapperAutoMapper/Slapper.AutoMapper
źródło
Chciałem, żeby to było tak proste, jak to tylko możliwe, moje rozwiązanie:
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(); }
Nadal wykonuję jedno wywołanie bazy danych i chociaż teraz wykonuję 2 zapytania zamiast jednego, drugie zapytanie używa złączenia WEWNĘTRZNEGO zamiast mniej optymalnego złączenia LEWEGO.
źródło
.Join(
ale tworzy wykres obiektu zamiast spłaszczonego wyniku.Niewielka modyfikacja odpowiedzi Andrew, która wykorzystuje Func do wybrania klucza nadrzędnego zamiast
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; }
Przykładowe użycie
conn.QueryParentChild<Product, Store, int>("sql here", prod => prod.Id, prod => prod.Stores)
źródło
class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } }
Zgodnie z tą odpowiedzią, w Dapper.Net nie ma wielu funkcji obsługi map. Zapytania zawsze zwracają jeden obiekt na wiersz bazy danych. Istnieje jednak alternatywne rozwiązanie.
źródło
(contact, phones) => { contact.Phones = phones; }
Musiałbym napisać filtr dla telefonów, których contactid pasuje do contactid. To jest dość nieefektywne.Oto prymitywne obejście
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; }
nie jest to w żadnym wypadku najbardziej efektywny sposób, ale pozwoli Ci zacząć działać. Spróbuję to zoptymalizować, kiedy będę miał okazję.
użyj tego w ten sposób:
conn.Query<Product, Store>("sql here", prod => prod.Stores);
pamiętaj, że obiekty muszą zostać zaimplementowane
GetHashCode
, na przykład w ten sposób:public override int GetHashCode() { return this.Id.GetHashCode(); }
źródło
Oto inna metoda:
Zamówienie (jedno) - OrderDetail (wiele)
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(); }
Źródło : http://dapper-tutorial.net/result-multi-mapping#example---query-multi-mapping-one-to-many
źródło