Jak napisać jedno do wielu zapytań w Dapper.Net?

80

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.

TCM
źródło
1
Jak wyglądają Twoje istoty?
gideon
2
Jak nie działa? Czy otrzymujesz wyjątek? Nieoczekiwane wyniki?
driis
1
Błąd nie ma znaczenia, dlatego nie zadałem sobie trudu, aby go opublikować. Otrzymuję: „{” Wartość nie może być null. \ R \ nNazwa parametru: con "}". Wiersz, który zgłasza błąd w SqlMapper, to: "il.Emit (OpCodes.Newobj, type.GetConstructor (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null));"
TCM,

Odpowiedzi:

162

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 MicroORMod 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 joinscelu 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 selectinstrukcji 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ę Contactsi każdy Contactmoż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; } // foreign key
    public string Number { get; set; }
}

Tabela SQL TestContact

wprowadź opis obrazu tutaj

Tabela SQL TestPhone

Zauważ, że ta tabela ma klucz obcy, ContactIDktóry odwołuje się do TestContacttabeli (odpowiada to List<TestPhone>w powyższym POCO).

wprowadź opis obrazu tutaj

SQL, który daje płaski wynik

W naszym zapytaniu SQL używamy tylu JOINinstrukcji, 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

wprowadź opis obrazu tutaj

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

wprowadź opis obrazu tutaj

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 TestContactma List<TestPhone>.

wprowadź opis obrazu tutaj

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ć Attributesdo 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 joini left joinwe 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.Automapperzawiera 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

Contango
źródło
1
Naprawdę nie podoba mi się konwencja przedrostków nazw tabel w całym twoim sql, chociaż nie obsługuje czegoś takiego jak "splitOn" Dappera?
tbone
3
Ta konwencja nazw tabeli jest wymagana przez Slapper.Automapper. Tak, Dapper obsługuje mapowanie bezpośrednio do POCO, ale wolę używać Slapper.Automapper, ponieważ kod jest tak czysty i łatwy w utrzymaniu.
Contango
2
Myślę, że użyłbym Slappera, gdybyś nie musiał aliasować wszystkich kolumn - zamiast tego w Twoim przykładzie chciałbym móc powiedzieć:, splitOn: "PhoneId" - czy to nie byłoby całkiem sporo łatwiejsze niż aliasowanie wszystkiego?
tbone
1
Naprawdę podoba mi się wygląd slappera, po prostu zastanawiam się, czy próbowałeś użyć lewego złączenia, gdy osoba nie ma numerów kontaktowych? Czy potrafisz sobie z tym dobrze radzić?
Nie podobało mi się
1
@tbone splitOn nie zawiera żadnych informacji o tym, gdzie w Twoim obiekcie należy ten element, dlatego slapper używa takiej ścieżki
Notoved
20

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.

Davy
źródło
5
Podoba mi się to podejście. Czysty elegancki i bardziej zrozumiałe mapowanie IMHO.
Avner
1
Wydaje się, że byłoby to łatwe do wprowadzenia do metody rozszerzającej, która wymaga kilku albm, jednej dla selektora kluczy i jednej dla selektora podrzędnego. Podobny do, .Join(ale tworzy wykres obiektu zamiast spłaszczonego wyniku.
AaronLS,
8

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)
Glina
źródło
W przypadku tego rozwiązania należy zwrócić uwagę na to, że klasa nadrzędna jest odpowiedzialna za tworzenie wystąpienia właściwości podrzędnej. class Parent { public List<Child> Children { get; set; } public Parent() { this.Children = new List<Child>(); } }
Clay,
1
To rozwiązanie jest doskonałe i sprawdziło się u nas. Musiałem dodać czek z dziećmi. Dodaj, aby sprawdzić, czy nie ma wartości null, na wypadek, gdyby nie zwrócono żadnych wierszy podrzędnych.
tlbignerd
7

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.

Damir Arh
źródło
Przepraszam, ale nie rozumiem, jak mam tego użyć w zapytaniu? Próbuje wysłać zapytanie do bazy danych 2 razy bez łączenia (i na przykład przy użyciu wpisanego na stałe 1). W przykładzie zwracana jest tylko 1 jednostka główna, która z kolei zawiera jednostki podrzędne. W moim przypadku chcę dołączyć do projektu (lista, która wewnętrznie zawiera listę). Jak mam to zrobić za pomocą wspomnianego linku? W linku, w którym linia mówi: (contact, phones) => { contact.Phones = phones; } Musiałbym napisać filtr dla telefonów, których contactid pasuje do contactid. To jest dość nieefektywne.
TCM,
@Anthony Spójrz na odpowiedź Mike'a. Wykonuje jedno zapytanie z dwoma zestawami wyników i następnie łączy je metodą Map. Oczywiście nie musisz zakodować wartości na stałe w swoim przypadku. Postaram się zebrać przykład za kilka godzin.
Damir Arh
1
w porządku, wreszcie działało. Dzięki! Nie wiem, jak wpłynęłoby to na wydajność przeszukiwania bazy danych dwukrotnie, co można by osiągnąć za pomocą pojedynczego sprzężenia.
TCM,
2
Nie rozumiem też, jakie zmiany musiałbym wprowadzić, gdyby były 3 stoły: p
TCM,
1
to jest do niczego… dlaczego u licha unikać łączenia?
GorillaApe
2

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();
    }
Andrew Bullock
źródło
11
Implementacja pamięci podręcznej jest wadliwa. Kody skrótu nie są unikalne - dwa obiekty mogą mieć ten sam kod skrótu. Może to prowadzić do wypełnienia listy obiektów przedmiotami, które należą do innego obiektu ..
stmax
2

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

Exocomp
źródło