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:
- 8 linii języka C #.
- Dość prosty SQL, który używa niektórych sprzężeń.
- Dwie niesamowite biblioteki.
Spostrzeżenie, które pozwoliło mi rozwiązać ten problem, polega na oddzieleniu MicroORM
od mapping 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
- Prostota . To mniej niż 8 linii kodu. Uważam, że jest to dużo łatwiejsze do zrozumienia, debugowania i zmiany.
- Mniej kodu . Kilka linijek kodu to wszystko Slapper.Automapper musi obsłużyć wszystko, co do niego rzucisz, nawet jeśli mamy złożone zagnieżdżone POCO (tj. POCO zawiera to,
List<MyClass1>
co z kolei zawiera List<MySubClass2>
, itp.).
- Szybkość . Obie te biblioteki mają nadzwyczajną ilość optymalizacji i buforowania, dzięki czemu działają prawie tak szybko, jak ręcznie dostrojone zapytania ADO.NET.
- Rozdzielenie obaw . Możemy zmienić MicroORM na inny, a mapowanie nadal działa i odwrotnie.
- Elastyczność . Slapper.Automapper obsługuje dowolnie zagnieżdżone hierarchie, nie jest ograniczony do kilku poziomów zagnieżdżenia. Możemy łatwo wprowadzać szybkie zmiany i wszystko będzie nadal działać.
- Debugowanie . Najpierw możemy zobaczyć, że zapytanie SQL działa poprawnie, a następnie możemy sprawdzić, czy wynik zapytania SQL jest poprawnie odwzorowany z powrotem na docelowe jednostki POCO.
- Łatwość programowania w SQL . Uważam, że tworzenie spłaszczonych zapytań w
inner joins
celu zwrócenia płaskich wyników jest znacznie łatwiejsze niż tworzenie wielu instrukcji wyboru ze zszywaniem po stronie klienta.
- Zoptymalizowane zapytania w SQL . W wysoce znormalizowanej bazie danych utworzenie płaskiego zapytania umożliwia silnikowi SQL zastosowanie zaawansowanych optymalizacji całości, co normalnie nie byłoby możliwe, gdyby zostało skonstruowanych i uruchomionych wiele małych pojedynczych zapytań.
- Zaufaj . Dapper to zaplecze StackOverflow i, cóż, Randy Burden jest trochę supergwiazdą. Czy muszę coś więcej mówić?
- Szybkość rozwoju. Byłem w stanie wykonać niezwykle złożone zapytania z wieloma poziomami zagnieżdżania, a czas tworzenia oprogramowania był dość krótki.
- Mniej błędów. Kiedyś to napisałem, po prostu zadziałało, a ta technika pomaga teraz zasilić firmę FTSE. Było tak mało kodu, że nie było nieoczekiwanego zachowania.
Niedogodności
- Zwrócono skalowanie powyżej 1 000 000 wierszy. Działa dobrze w przypadku zwracania <100 000 wierszy. Jeśli jednak przywracamy> 1 000 000 wierszy, aby zmniejszyć ruch między nami a serwerem SQL, nie powinniśmy go spłaszczyć za pomocą
inner join
(co przywraca duplikaty), zamiast tego powinniśmy użyć wielu select
instrukcji i zszyć wszystko z powrotem w po stronie klienta (zobacz inne odpowiedzi na tej stronie).
- Ta technika jest zorientowana na zapytania . Nie używałem tej techniki do zapisywania w bazie danych, ale jestem pewien, że Dapper jest więcej niż zdolny do zrobienia tego przy dodatkowej pracy, ponieważ sam StackOverflow używa Dappera jako warstwy dostępu do danych (DAL).
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życiu 3 lines of code to return the dynamic
.
Dapper + Slapper.Automapper
: 2,463 milisekund na zapytanie przy użyciu dodatkowego 3 lines of code for the query + mapping from dynamic to POCO Entities
.
Przykład praktyczny
W tym przykładzie mamy listę Contacts
i każdy Contact
może mieć jeden lub więcej phone 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; }
public string Number { get; set; }
}
Tabela SQL TestContact
Tabela SQL TestPhone
Zauważ, że ta tabela ma klucz obcy, ContactID
który odwołuje się do TestContact
tabeli (odpowiada to List<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 =
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
{
dynamic test = conn.Query<dynamic>(sql);
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żdy TestContact
ma List<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
i left 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