Utilisation correcte du multimapping dans Dapper

111

J'essaie d'utiliser la fonctionnalité Multimapping de dapper pour renvoyer une liste de ProductItems 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 le suivant

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 semble que je doive 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 renvoie null. Est-ce que je ne comprends pas la fonctionnalité de base de la fonction de multi-cartographie. Je ne veux pas avoir à ajouter une liste complète des noms de colonnes à chaque fois.

Richard Forrest
la source
comment afficher les deux tables dans datagridview alors? un petit exemple sera très apprécié.
Ankur Soni

Réponses:

184

Je viens de lancer 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é comme point de partage, il vaut par défaut Id. S'il y a 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 | ProductName | AccountOpened | CustomerId | Nom du client
--------------------------------------- ----------- --------------

Dapper a besoin de savoir comment diviser les colonnes dans cet ordre en 2 objets. Un coup d'œil rapide montre que le client commence par la colonne CustomerId, par conséquent splitOn: CustomerId.

Il y a une grande mise en garde ici, si l'ordre des colonnes dans la table sous-jacente est inversé pour une raison quelconque:

ProductID | ProductName | AccountOpened | CustomerName | N ° de client  
--------------------------------------- ----------- --------------

splitOn: CustomerId entraînera un nom de client nul.

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

Sam Safran
la source
2
Merci Sam. Ouais votre droit c'était l'ordre de retour des colonnes qui était le problème avec CustomerName | CustomerId renvoyé CustomerName revenait à null.
Richard Forrest
18
Une chose à retenir est que vous ne pouvez pas avoir d'espaces dans le spliton, c'est-à-dire CustomerId,CustomerNamenon CustomerId, CustomerName, car Dapper ne fait pas Trimles résultats de la division de chaîne. Il lancera simplement l'erreur générique de spliton. M'a rendu fou un jour.
jes
2
@vaheeds vous devez TOUJOURS utiliser des noms de colonnes et ne jamais utiliser d'étoile, cela donne moins de travail à SQL, et vous n'obtenez pas de situations où l'ordre des colonnes est incorrect, comme dans ce cas.
Harag
3
@vaheeds - en ce qui concerne l'id, l'id, l'ID en regardant le code dapper, il n'est pas sensible à la casse, et il coupe également le texte pour le splitOn - c'est la v1.50.2.0 de dapper.
Harag
2
Pour quiconque se demande, au cas où vous auriez à diviser une requête en 3 objets: sur une colonne nommée "Id" et sur une colonne nommée "somethingId", assurez-vous d'inclure le premier "Id" dans la clause de fractionnement. Même si Dapper se divise par défaut sur "Id", dans ce cas, il doit être défini explicitement.
Sbu
27

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 divise juste trop tôt (peut-être), car les colonnes seraient:

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

Cela rend le paramètre spliton: pas si utile, surtout lorsque vous ne savez pas dans quel ordre les colonnes sont retournées. Bien sûr, vous pouvez spécifier manuellement les colonnes ... mais nous sommes en 2017 et nous le faisons rarement plus 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, est simplement d'utiliser un alias pour Id, et de ne jamais spécifier spliton (en utilisant l'ID par défaut de Dapper).

select 
p.*,

c.CustomerID AS Id,
c.*

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

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Mieux encore, vous montrez clairement dans un minimum de sql quelles colonnes sont associées à quel objet. Dapper fait le reste.

BlackjacketMack
la source
Il s'agit d'une approche concise tant qu'aucune table n'a de champs Id.
Bernard Vander Beken le
Avec cette approche, une table peut toujours avoir un champ Id ... mais ce devrait être le PK. Vous n'auriez tout simplement pas à créer l'alias, donc c'est en fait un peu moins de travail. (Je pense qu'il est très inhabituel (mauvaise forme?) D'avoir une colonne appelée 'Id' qui n'est pas le PK.)
BlackjacketMack
5

En supposant la structure suivante où '|' est le point de division et Ts sont les entités auxquelles le mappage doit être appliqué.

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

Voici la requête pimpante que vous devrez écrire.

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

Nous voulons donc que TFirst mappe col_1 col_2 col_3, pour TSecond le col_n col_m ...

L'expression splitOn se traduit par:

Commencez le mappage de toutes les colonnes dans TFrist jusqu'à ce que vous trouviez une colonne nommée ou aliasée comme «col_3», et incluez également «col_3» dans le résultat du mappage.

Ensuite, commencez à mapper dans TSecond toutes les colonnes à partir de 'col_n' et continuez à mapper jusqu'à ce qu'un nouveau séparateur soit trouvé, qui dans ce cas est 'col_A' et marque le début du mappage TThird et ainsi d'un.

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 être nommés de la même manière), si les noms de colonnes résultant de la requête SQL sont différents, vous pouvez les alias en utilisant le 'AS [ Expression Some_Alias_Name].

Boris
la source
2

Il y a une autre mise en garde. Si le champ CustomerId est nul (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 un autre avertissement / 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é (Customer dans ce cas). Pour démontrer l'utilisation de cette classe avec SQL précédent:

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
la source
existe-t-il une solution au deuxième exemple en plus d'ajouter le Customerid à la classe? J'ai un problème où j'ai besoin d'un objet nul, mais cela me donne un objet vide. ( stackoverflow.com/questions/27231637/… )
jmzagorski
1

Je fais cela de manière générique dans mon repo, 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 vos propriétés de clé étrangère sont le nom de votre objet enfant + "Id", par exemple UnitId.
  • Je l'ai mappé seulement 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;
    }
Dylan Hayes
la source
0

Si vous devez mapper une grande entité, écrire chaque champ doit être une tâche difficile.

J'ai essayé la réponse @BlackjacketMack, mais l'une de mes tables a une colonne Id d'autres non (je sais que c'est un problème de conception de base de données, mais ...) alors cela insère un fractionnement supplémentaire sur dapper, c'est pourquoi

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Ça ne marche pas pour moi. Ensuite, j'ai terminé avec un petit changement à ceci, insérez simplement un point de partage avec un nom qui ne correspond à aucun champ sur les tables, en cas de changement as Idpar as _SplitPoint_, le script sql final ressemble à ceci:

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

Ensuite, dans dapper, ajoutez juste un splitOn comme ceci

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
la source