Uso corretto del Multimapping in Dapper


111

Sto cercando di utilizzare la funzionalità Multimapping di dapper per restituire un elenco di ProductItem e clienti associati.

[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; }
}

Il mio codice azzimato è il seguente

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"
);

Funziona bene ma mi sembra di dover aggiungere l'elenco completo delle colonne al parametro splitOn per restituire tutte le proprietà dei clienti. Se non aggiungo "CustomerName" restituisce null. Non riesco a capire la funzionalità principale della funzionalità di multimappatura. Non voglio dover aggiungere ogni volta un elenco completo di nomi di colonne.


come si fa a mostrare effettivamente entrambe le tabelle in datagridview allora? un piccolo esempio sarà molto apprezzato.
Ankur Soni

Risposte:


184

Ho appena eseguito un test che funziona bene:

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);

Il parametro splitOn deve essere specificato come punto di divisione, il valore predefinito è Id. Se sono presenti più punti di divisione, sarà necessario aggiungerli in un elenco delimitato da virgole.

Supponi che il tuo recordset abbia questo aspetto:

ProductID | ProductName | AccountOpened | CustomerId | Nome del cliente
--------------------------------------- ----------- --------------

Dapper deve sapere come dividere le colonne in questo ordine in 2 oggetti. Uno sguardo superficiale mostra che il cliente inizia dalla colonna CustomerId, quindi splitOn: CustomerId.

C'è un grosso avvertimento qui, se l'ordine delle colonne nella tabella sottostante viene capovolto per qualche motivo:

ProductID | ProductName | AccountOpened | CustomerName | Identificativo del cliente  
--------------------------------------- ----------- --------------

splitOn: CustomerId risulterà in un nome cliente nullo.

Se specifichi CustomerId,CustomerNamecome punti di divisione, dapper presume che tu stia cercando di dividere il set di risultati in 3 oggetti. La prima parte dall'inizio, la seconda inizia alle CustomerId, la terza alle CustomerName.


2
Grazie Sam. Sì, hai ragione, era l'ordine di restituzione delle colonne che era il problema con CustomerName | CustomerId restituito CustomerName tornato null.
Richard Forrest

18
Una cosa da ricordare è che non puoi avere spazi in spliton, cioè CustomerId,CustomerNameno CustomerId, CustomerName, poiché Dapper non è Trimil risultato della divisione della stringa. Lancia solo l'errore di spliton generico. Un giorno mi ha fatto impazzire.
jes

2
@vaheeds dovresti SEMPRE usare i nomi delle colonne e non usare mai una stella, dà sql meno lavoro da fare e non ottieni situazioni in cui l'ordine delle colonne è sbagliato, come in questo caso.
Harag

3
@vaheeds - per quanto riguarda l'id, l'ID, l'ID guardando il codice dapper non fa distinzione tra maiuscole e minuscole e taglia anche il testo per lo splitOn - questa è la v1.50.2.0 di dapper.
Harag

2
Per chiunque si chieda, nel caso in cui sia necessario dividere una query in 3 oggetti: su una colonna denominata "Id" e su una colonna denominata "somethingId", assicurarsi di includere il primo "Id" nella clausola di divisione. Anche se Dapper si divide di default su "Id", in questo caso deve essere impostato esplicitamente.
Sbu

27

Le nostre tabelle hanno un nome simile alla tua, dove qualcosa come "CustomerID" potrebbe essere restituito due volte utilizzando un'operazione "select *". Pertanto, Dapper sta facendo il suo lavoro ma si divide troppo presto (forse), perché le colonne sarebbero:

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

Questo rende il parametro spliton: non così utile, specialmente quando non sei sicuro in quale ordine vengono restituite le colonne. Ovviamente potresti specificare manualmente le colonne ... ma è il 2017 e raramente lo facciamo più per gli oggetti di base.

Quello che facciamo, e ha funzionato alla grande per migliaia di query per molti anni, è semplicemente utilizzare un alias per Id e non specificare mai spliton (utilizzando l''Id 'predefinito di Dapper).

select 
p.*,

c.CustomerID AS Id,
c.*

...Ecco! Dapper si dividerà solo in base all'ID per impostazione predefinita e tale ID verrà visualizzato prima di tutte le colonne del cliente. Ovviamente aggiungerà una colonna in più al tuo set di risultati restituito, ma questo è un sovraccarico estremamente minimo per l'utilità aggiuntiva di sapere esattamente quali colonne appartengono a quale oggetto. E puoi facilmente espandere questo. Hai bisogno di informazioni sull'indirizzo e sul paese?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Soprattutto, stai chiaramente mostrando in una quantità minima di sql quali colonne sono associate a quale oggetto. Dapper fa il resto.


Questo è un approccio conciso purché nessuna tabella abbia campi Id.
Bernard Vander Beken,

Con questo approccio una tabella può ancora avere un campo Id ... ma dovrebbe essere il PK. Semplicemente non dovresti creare l'alias, quindi in realtà è un po 'meno lavoro. (Penso che sia molto insolito (brutta forma?) Avere una colonna chiamata "Id" che non è il PK.)
BlackjacketMack

5

Supponendo la seguente struttura dove '|' è il punto di divisione e Ts sono le entità a cui deve essere applicata la mappatura.

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

Di seguito è la query azzimata che dovrai scrivere.

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")

Quindi vogliamo che TFirst mappi col_1 col_2 col_3, per TSecond il col_n col_m ...

L'espressione splitOn si traduce in:

Inizia la mappatura di tutte le colonne in TFrist fino a trovare una colonna denominata o con alias "col_3" e includi anche "col_3" nel risultato della mappatura.

Quindi inizia a mappare in TSecond tutte le colonne a partire da 'col_n' e continua a mappare fino a trovare un nuovo separatore, che in questo caso è 'col_A' e segna l'inizio della mappatura TThird e così uno.

Le colonne della query sql e gli oggetti di scena dell'oggetto di mappatura sono in una relazione 1: 1 (il che significa che dovrebbero essere denominati allo stesso modo), se i nomi delle colonne risultanti dalla query sql sono diversi, puoi creare un alias utilizzando l'AS [ Some_Alias_Name] 'espressione.


2

C'è ancora un avvertimento. Se il campo CustomerId è nullo (in genere nelle query con join sinistro) Dapper crea ProductItem con Customer = null. Nell'esempio sopra:

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); 

E anche un altro avvertimento / trappola. Se non mappi il campo specificato in splitOn e quel campo contiene null, Dapper crea e riempie l'oggetto correlato (Customer in questo caso). Per dimostrare l'uso di questa classe con sql precedente:

public class Customer
{
    //public decimal CustomerId { get; set; }
    public string CustomerName { get; set; }
}
...
Debug.Assert(item.Customer != null);
Debug.Assert(item.Customer.CustomerName == "n");  

c'è una soluzione al secondo esempio oltre ad aggiungere il Customerid alla classe? Sto riscontrando un problema in cui ho bisogno di un oggetto nullo, ma mi sta dando un oggetto vuoto. ( Stackoverflow.com/questions/27231637/... )
jmzagorski

1

Lo faccio genericamente nel mio repository, funziona bene per il mio caso d'uso. Ho pensato di condividere. Forse qualcuno lo estenderà ulteriormente.

Alcuni svantaggi sono:

  • Ciò presuppone che le proprietà della chiave esterna siano il nome dell'oggetto figlio + "Id", ad esempio UnitId.
  • Ce l'ho solo mappando 1 oggetto figlio al genitore.

Il codice:

    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;
    }

0

Se è necessario mappare un'entità di grandi dimensioni, scrivere ogni campo deve essere un compito difficile.

Ho provato la risposta @BlackjacketMack, ma una delle mie tabelle ha una colonna Id, altre no (so che è un problema di progettazione del DB, ma ...) quindi questo inserisce una divisione extra su dapper, ecco perché

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Non funziona per me. Poi ho terminato con una piccola modifica a questo, basta inserire un punto di divisione con un nome che non corrisponde a nessun campo sulle tabelle, in alcuni casi modificato as Idda as _SplitPoint_, lo script sql finale è simile a questo:

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

Quindi in dapper aggiungi solo uno splitOn come questo

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();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.