web-dev-qa-db-fra.com

Utilisation correcte du multimappage dans Dapper

J'essaie d'utiliser la fonctionnalité de mappage multiple de dapper pour renvoyer une liste de produits et de clients associés.

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

Mon code dapper est comme suit

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

Cela fonctionne bien, mais il me semble devoir ajouter la liste complète des colonnes au paramètre splitOn pour renvoyer toutes les propriétés des clients. Si je n'ajoute pas "CustomerName", il retourne null. Est-ce que je comprends mal la fonctionnalité de base de la fonctionnalité de mappage multiple? Je ne veux pas avoir à ajouter une liste complète de noms de colonnes à chaque fois.

95
Richard Forrest

Je viens de faire un test qui fonctionne bien:

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

Le paramètre splitOn doit être spécifié en tant que point de partage. Sa valeur par défaut est Id. S'il existe plusieurs points de partage, vous devrez les ajouter dans une liste délimitée par des virgules.

Dites que votre jeu d'enregistrements ressemble à ceci:

 ProductID | Nom du produit | AccountOpened | CustomerId | CustomerName 
 --------------------------------------- ----- -------------------- 

Dapper doit savoir comment scinder les colonnes dans cet ordre en 2 objets. Un regard superficiel montre que le client commence à la colonne CustomerId, d’où splitOn: CustomerId.

Il y a un grand avertissement ici, si l'ordre des colonnes dans la table sous-jacente est inversé pour une raison quelconque:

 ProductID | Nom du produit | AccountOpened | CustomerName | N ° de client  
---------------------------------------   ----- -------------------- 

splitOn: CustomerId donnera un nom de client nul.

Si vous spécifiez CustomerId,CustomerName en tant que points de partage, dapper suppose que vous essayez de fractionner le jeu de résultats en 3 objets. Le premier commence au début, le second commence à CustomerId, le troisième à CustomerName.

162
Sam Saffron

Nos tables sont nommées de la même manière que la vôtre, où quelque chose comme "CustomerID" peut être renvoyé deux fois en utilisant une opération "select *". Par conséquent, Dapper fait son travail mais se sépare trop tôt (éventuellement), car les colonnes seraient:

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

Cela rend le paramètre spliton: paramètre pas très utile, en particulier lorsque vous n'êtes pas sûr de l'ordre dans lequel les colonnes sont retournées. Bien sûr, vous pouvez spécifier manuellement des colonnes ... mais nous sommes en 2017 et nous ne le faisons que très rarement pour les objets de base.

Ce que nous faisons, et cela a très bien fonctionné pour des milliers de requêtes pendant de nombreuses années, consiste simplement à utiliser un alias pour Id et à ne jamais spécifier spliton (à l'aide de "l'Id" par défaut de Dapper).

select 
p.*,

c.CustomerID AS Id,
c.*

... voila! Dapper ne se divisera que par défaut sur Id, et cet ID apparaît avant toutes les colonnes du client. Bien sûr, cela ajoutera une colonne supplémentaire à votre jeu de résultats, mais il s’agit là d’une surcharge extrêmement minimale pour l’utilitaire supplémentaire qui consiste à savoir exactement quelles colonnes appartiennent à quel objet. Et vous pouvez facilement développer cela. Besoin d'adresse et d'informations sur le pays?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Mieux encore, vous indiquez clairement dans une quantité minimale de sql quelles colonnes sont associées à quel objet. Dapper fait le reste.

22
BlackjacketMack

Il y a une autre mise en garde. Si le champ CustomerId est null (généralement dans les requêtes avec jointure à gauche), Dapper crée ProductItem avec Customer = null. Dans l'exemple ci-dessus:

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

Et même une autre mise en garde/piège. Si vous ne mappez pas le champ spécifié dans splitOn et que ce champ contient null, Dapper crée et remplit l'objet associé (le client dans ce cas). Pour démontrer utiliser cette classe avec le précédent 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");  
2
Frantisek Bachan

Je le fais génériquement dans mon référentiel, cela fonctionne bien pour mon cas d'utilisation. Je pensais partager. Peut-être que quelqu'un étendra cela plus loin.

Certains inconvénients sont:

  • Cela suppose que les propriétés de votre clé étrangère sont le nom de votre objet enfant + "Id", par exemple. UnitId.
  • Je l'ai seulement mappant 1 objet enfant au parent.

Le code:

    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;
    }
1
Dylan Hayes

En supposant la structure de requête SQL suivante (représentation des noms de colonne, les valeurs ne sont pas pertinentes)

col_1 col_2 col_3 | col_n col_m | col_A col_B col_C | col_9 col_8

Donc, dans dapper, vous utiliserez la définition suivante de Query (QueryAsync)

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

où nous voulons que TFirst mappe la première partie, la deuxième et la deuxième.

L'expression splitOn se traduit par:

Mappez toutes les colonnes sur TFrist jusqu'à ce que vous trouviez une colonne nommée ou alias "col_3", incluez également la colonne dans le mappage.

Puis mappez sur TSecond en commençant par col_n jusqu'à la fin ou un nouveau séparateur trouvé (incluez-le également dans le mappage col_n)

Puis mappez vers TThird en commençant par col_A jusqu'à la fin ou un nouveau séparateur trouvé (incluez-le également dans la cartographie col_A)

Puis mappez vers TFourth en commençant par col_9 jusqu'à la fin ou un nouveau séparateur trouvé (incluez-le également dans la cartographie col_9)

Les colonnes de la requête SQL et les accessoires de l'objet de mappage sont dans une relation 1: 1 (ce qui signifie qu'ils doivent porter le même nom). Si les noms des colonnes résultant de la requête SQL sont différents, vous utiliserez des alias AS [Nom_Alias_Ancien]

1
MCR