Prawidłowe użycie Multimapping w Dapper

111

Próbuję użyć funkcji Multimapping dapper, aby zwrócić listę ProductItems i powiązanych klientów.

[Table("Product")]
public class ProductItem
{
    public decimal ProductID { get; set; }        
    public string ProductName { get; set; }
    public string AccountOpened { get; set; }
    public Customer Customer { get; set; }
} 

public class Customer
{
    public decimal CustomerId { get; set; }
    public string CustomerName { get; set; }
}

Mój elegancki kod jest następujący

var sql = @"select * from Product p 
            inner join Customer c on p.CustomerId = c.CustomerId 
            order by p.ProductName";

var data = con.Query<ProductItem, Customer, ProductItem>(
    sql,
    (productItem, customer) => {
        productItem.Customer = customer;
        return productItem;
    },
    splitOn: "CustomerId,CustomerName"
);

Działa to dobrze, ale wydaje mi się, że muszę dodać pełną listę kolumn do parametru splitOn, aby zwrócić wszystkie właściwości klientów. Jeśli nie dodam „CustomerName”, zwraca wartość null. Czy nie rozumiem podstawowych funkcji funkcji multimapping. Nie chcę za każdym razem dodawać pełnej listy nazw kolumn.

Richard Forrest
źródło
jak w takim razie pokazać obie tabele w datagridview? mały przykład będzie bardzo mile widziany.
Ankur Soni

Odpowiedzi:

184

Właśnie przeprowadziłem test, który działa dobrze:

var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(1 as decimal) CustomerId, 'name' CustomerName";

var item = connection.Query<ProductItem, Customer, ProductItem>(sql,
    (p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();

item.Customer.CustomerId.IsEqualTo(1);

Parametr splitOn musi być określony jako punkt podziału, domyślnie jest to Id. Jeśli istnieje wiele punktów podziału, musisz dodać je na liście rozdzielanej przecinkami.

Powiedz, że Twój zestaw rekordów wygląda następująco:

ProductID | Nazwa produktu | Konto otwarte | CustomerId | CustomerName
--------------------------------------- ----------- --------------

Dapper musi wiedzieć, jak podzielić kolumny w tej kolejności na 2 obiekty. Pobieżne spojrzenie pokazuje, że klient rozpoczyna się w kolumnie CustomerId, stąd splitOn: CustomerId.

Istnieje duże zastrzeżenie, jeśli kolejność kolumn w tabeli bazowej zostanie z jakiegoś powodu odwrócona:

ProductID | Nazwa produktu | Konto otwarte | CustomerName | Identyfikator klienta  
--------------------------------------- ----------- --------------

splitOn: CustomerId spowoduje, że nazwa klienta będzie pusta.

Jeśli określisz CustomerId,CustomerNamejako punkty podziału, dapper zakłada, że ​​próbujesz podzielić zestaw wyników na 3 obiekty. Pierwszy zaczyna się od początku, drugi zaczyna się o CustomerId, trzeci o CustomerName.

Sam Saffron
źródło
2
Dzięki, Sam. Tak, masz rację, to była kolejność zwrotu kolumn, która była przyczyną problemu z CustomerName | Zwracany identyfikator klienta CustomerName wracał do wartości null.
Richard Forrest,
18
Jedną rzeczą do zapamiętania jest to, że nie możesz mieć spacji w spliton, tj. CustomerId,CustomerNameNie CustomerId, CustomerName, ponieważ Dapper nie ma Trimwyników podziału łańcucha. Po prostu wyrzuci ogólny błąd spliton. Pewnego dnia oszalałem.
jes
2
@vaheeds powinieneś ZAWSZE używać nazw kolumn i nigdy nie używać gwiazdki, to daje sql mniej pracy i nie zdarzają się sytuacje, w których kolejność kolumn jest nieprawidłowa, jak w tym przypadku.
Harag
3
@vaheeds - jeśli chodzi o id, Id, ID patrząc na elegancki kod, nie ma rozróżniania wielkości liter, a także przycina tekst dla splitOn - to jest v1.50.2.0 dapper.
Harag
2
Dla każdego, kto się zastanawia, gdybyś musiał podzielić zapytanie na 3 obiekty: w jednej kolumnie o nazwie „Id” i jednej kolumnie o nazwie „somethingId”, pamiętaj o umieszczeniu pierwszego „Id” w klauzuli podziału. Mimo że Dapper dzieli się domyślnie na „Id”, w tym przypadku należy to ustawić jawnie.
Sbu
27

Nasze tabele mają podobne nazwy do twoich, gdzie coś w rodzaju „IDklienta” może zostać zwrócone dwukrotnie za pomocą operacji „select *”. Dlatego Dapper wykonuje swoją pracę, ale dzieli się zbyt wcześnie (prawdopodobnie), ponieważ kolumny wyglądałyby następująco:

(select * might return):
ProductID,
ProductName,
CustomerID, --first CustomerID
AccountOpened,
CustomerID, --second CustomerID,
CustomerName.

To sprawia, że ​​parametr spliton: nie jest tak przydatny, zwłaszcza gdy nie masz pewności, w jakiej kolejności zwracane są kolumny. Oczywiście możesz ręcznie określić kolumny ... ale jest rok 2017 i po prostu rzadko robimy to już dla podstawowego obiektu.

To, co robimy i działało świetnie dla tysięcy zapytań przez wiele lat, polega po prostu na używaniu aliasu dla Id i nigdy nie określamy spliton (używając domyślnego „Id” Dappera).

select 
p.*,

c.CustomerID AS Id,
c.*

... voila! Dapper domyślnie podzieli się tylko na identyfikator, a ten identyfikator występuje przed wszystkimi kolumnami klienta. Oczywiście doda to dodatkową kolumnę do zwracanego zestawu wyników, ale jest to niezwykle minimalne obciążenie dla dodatkowej użyteczności polegającej na dokładnym poznaniu, które kolumny należą do danego obiektu. Możesz to łatwo rozszerzyć. Potrzebujesz informacji o adresie i kraju?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

A co najważniejsze, w minimalnej ilości sql wyraźnie pokazujesz, które kolumny są skojarzone z jakim obiektem. Dapper zajmie się resztą.

BlackjacketMack
źródło
Jest to zwięzłe podejście, o ile żadna tabela nie ma pól identyfikatora.
Bernard Vander Beken
Przy takim podejściu tabela może nadal mieć pole Id ... ale powinno to być PK. Po prostu nie musiałbyś tworzyć aliasu, więc w rzeczywistości jest to trochę mniej pracy. (Myślę, że to bardzo niezwykłe (zła forma?), Aby mieć kolumnę o nazwie „Id”, która nie jest PK.)
BlackjacketMack
5

Zakładając następującą strukturę, gdzie „|” jest punktem podziału, a Ts to jednostki, do których należy zastosować mapowanie.

       TFirst         TSecond         TThird           TFourth
------------------+-------------+-------------------+------------
col_1 col_2 col_3 | col_n col_m | col_A col_B col_C | col_9 col_8
------------------+-------------+-------------------+------------

Poniżej znajduje się eleganckie zapytanie, które będziesz musiał napisać.

Query<TFirst, TSecond, TThird, TFourth, TResut> (
    sql : query,
    map: Func<TFirst, TSecond, TThird, TFourth, TResut> func,
    parma: optional,
    splitOn: "col_3, col_n, col_A, col_9")

Więc chcemy, aby TFirst mapował col_1 col_2 col_3, a dla TSecond col_n col_m ...

Wyrażenie splitOn przekłada się na:

Rozpocznij mapowanie wszystkich kolumn do TFrist, aż znajdziesz kolumnę o nazwie lub aliasie jako „col_3”, a także dołącz „col_3” do wyniku mapowania.

Następnie rozpocznij mapowanie w TSecond wszystkich kolumn zaczynając od 'col_n' i kontynuuj mapowanie, aż zostanie znaleziony nowy separator, którym w tym przypadku jest 'col_A' i oznacza początek mapowania TThird i tak jeden.

Kolumny zapytania sql i właściwości obiektu mapującego są w relacji 1: 1 (co oznacza, że ​​powinny mieć takie same nazwy), jeśli nazwy kolumn wynikające z zapytania sql są różne, możesz aliasować je za pomocą 'AS [ Some_Alias_Name] ”wyrażenie.

Boris
źródło
2

Jest jeszcze jedno zastrzeżenie. Jeśli pole CustomerId ma wartość null (zwykle w zapytaniach z lewym złączeniem), Dapper tworzy ProductItem z Customer = null. W powyższym przykładzie:

var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(null as decimal) CustomerId, 'n' CustomerName";
var item = connection.Query<ProductItem, Customer, ProductItem>(sql, (p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();
Debug.Assert(item.Customer == null); 

I jeszcze jedno zastrzeżenie / pułapka. Jeśli nie zmapujesz pola określonego w splitOn i to pole zawiera wartość null, Dapper tworzy i wypełnia powiązany obiekt (w tym przypadku Customer). Aby zademonstrować użycie tej klasy z poprzednim sql:

public class Customer
{
    //public decimal CustomerId { get; set; }
    public string CustomerName { get; set; }
}
...
Debug.Assert(item.Customer != null);
Debug.Assert(item.Customer.CustomerName == "n");  
Frantisek Bachan
źródło
czy istnieje rozwiązanie dla drugiego przykładu poza dodaniem identyfikatora klienta do klasy? Mam problem, w którym potrzebuję obiektu zerowego, ale daje mi on pusty obiekt. ( stackoverflow.com/questions/27231637/… )
jmzagorski
1

Robię to ogólnie w moim repozytorium, działa dobrze w moim przypadku użycia. Myślałem, że się podzielę. Może ktoś to jeszcze przedłuży.

Niektóre wady to:

  • Zakłada się, że właściwościami klucza obcego są nazwa obiektu podrzędnego + „Id”, np. UnitId.
  • Mam to tylko mapowanie 1 obiektu podrzędnego do rodzica.

Kod:

    public IEnumerable<TParent> GetParentChild<TParent, TChild>()
    {
        var sql = string.Format(@"select * from {0} p 
        inner join {1} c on p.{1}Id = c.Id", 
        typeof(TParent).Name, typeof(TChild).Name);

        Debug.WriteLine(sql);

        var data = _con.Query<TParent, TChild, TParent>(
            sql,
            (p, c) =>
            {
                p.GetType().GetProperty(typeof (TChild).Name).SetValue(p, c);
                return p;
            },
            splitOn: typeof(TChild).Name + "Id");

        return data;
    }
Dylan Hayes
źródło
0

Jeśli musisz zmapować dużą jednostkę, napisanie każdego pola musi być trudnym zadaniem.

Próbowałem odpowiedzieć @BlackjacketMack, ale jedna z moich tabel ma kolumnę Id, inne nie (wiem, że to problem z projektowaniem DB, ale ...) to wstaw dodatkowy podział na elegancki, dlatego

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Nie działa na mnie. Następnie zakończyłem małą zmianą w tym, po prostu wstaw punkt podziału z nazwą, która nie pasuje do żadnego pola w tabelach, W przypadku zmiany as Ido as _SplitPoint_, ostateczny skrypt sql wygląda następująco:

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

Następnie w zgrabny sposób dodaj tylko jeden splitOn jako ten

cmd =
    "SELECT Materials.*, " +
    "   Product.ItemtId as _SplitPoint_," +
    "   Product.*, " +
    "   MeasureUnit.IntIdUM as _SplitPoint_, " +
    "   MeasureUnit.* " +
    "FROM   Materials INNER JOIN " +
    "   Product ON Materials.ItemtId = Product.ItemtId INNER JOIN " +
    "   MeasureUnit ON Materials.IntIdUM = MeasureUnit.IntIdUM " +
List < Materials> fTecnica3 = (await dpCx.QueryAsync<Materials>(
        cmd,
        new[] { typeof(Materials), typeof(Product), typeof(MeasureUnit) },
        (objects) =>
        {
            Materials mat = (Materials)objects[0];
            mat.Product = (Product)objects[1];
            mat.MeasureUnit = (MeasureUnit)objects[2];
            return mat;
        },
        splitOn: "_SplitPoint_"
    )).ToList();
Juan Pablo Gomez
źródło