Uso correcto de Multimapping en Dapper

111

Estoy tratando de usar la función Multimapping de dapper para devolver una lista de ProductItems y Clientes asociados.

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

Mi código elegante es el siguiente

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

Esto funciona bien, pero parece que tengo que agregar la lista de columnas completa al parámetro splitOn para devolver todas las propiedades de los clientes. Si no agrego "CustomerName", devuelve nulo. ¿No entiendo bien la funcionalidad principal de la función de mapas múltiples? No quiero tener que agregar una lista completa de nombres de columna cada vez.

Richard Forrest
fuente
¿Cómo mostrar realmente ambas tablas en datagridview entonces? un pequeño ejemplo será muy apreciado.
Ankur Soni

Respuestas:

184

Acabo de ejecutar una prueba que funciona 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);

El parámetro splitOn debe especificarse como el punto de división, por defecto es Id. Si hay varios puntos de división, deberá agregarlos en una lista delimitada por comas.

Digamos que su conjunto de registros se ve así:

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

Dapper necesita saber cómo dividir las columnas en este orden en 2 objetos. Una mirada superficial muestra que el Cliente comienza en la columna CustomerId, por lo tanto splitOn: CustomerId.

Aquí hay una gran advertencia, si el orden de las columnas en la tabla subyacente se invierte por alguna razón:

ProductID | ProductName | AccountOpened | CustomerName | Identificación del cliente  
--------------------------------------- ----------- --------------

splitOn: CustomerId dará como resultado un nombre de cliente nulo.

Si especifica CustomerId,CustomerNamecomo puntos de división, Dapper asume que está tratando de dividir el conjunto de resultados en 3 objetos. Primero comienza al principio, segundo comienza en CustomerId, tercero en CustomerName.

Sam Saffron
fuente
2
Gracias Sam. Sí, tiene razón, fue el orden de devolución de las columnas el problema con CustomerName | CustomerId devuelto CustomerName estaba volviendo nulo.
Richard Forrest
18
Una cosa para recordar es que no puede tener espacios en spliton, es decir , CustomerId,CustomerNameno CustomerId, CustomerName, ya que Dapper no tiene Trimlos resultados de la división de cadenas. Simplemente arrojará el error genérico de división. Me volvió loco un día.
jes
2
@vaheeds SIEMPRE debe usar nombres de columna y nunca usar una estrella, le da a sql menos trabajo que hacer y no obtiene situaciones en las que el orden de las columnas es incorrecto, como en este caso.
Harag
3
@vaheeds: con respecto a la identificación, la identificación, la identificación, mirando el código elegante, no distingue entre mayúsculas y minúsculas, y también recorta el texto para splitOn: esto es v1.50.2.0 de elegante.
Harag
2
Para cualquiera que se pregunte, en caso de que tenga que dividir una consulta en 3 objetos: en una columna llamada "Id" y en una columna llamada "somethingId", asegúrese de incluir el primer "Id" en la cláusula de división. Aunque Dapper se divide de forma predeterminada en "Id", en este caso debe establecerse explícitamente.
Sbu
27

Nuestras tablas se nombran de manera similar a la suya, donde algo como "CustomerID" podría devolverse dos veces usando una operación 'select *'. Por lo tanto, Dapper está haciendo su trabajo pero simplemente dividiendo demasiado pronto (posiblemente), porque las columnas serían:

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

Esto hace que el parámetro spliton: no sea tan útil, especialmente cuando no está seguro en qué orden se devuelven las columnas. Por supuesto, podría especificar columnas manualmente ... pero estamos en 2017 y rara vez lo hacemos para obtener objetos básicos.

Lo que hacemos, y ha funcionado muy bien para miles de consultas durante muchos años, es simplemente usar un alias para Id y nunca especificar spliton (usando el 'Id' predeterminado de Dapper).

select 
p.*,

c.CustomerID AS Id,
c.*

... ¡voilá! Dapper solo se dividirá en Id de forma predeterminada, y ese Id aparece antes de todas las columnas de Cliente. Por supuesto, agregará una columna adicional a su conjunto de resultados de retorno, pero esa es una sobrecarga extremadamente mínima para la utilidad adicional de saber exactamente qué columnas pertenecen a qué objeto. Y puedes expandir esto fácilmente. ¿Necesita información sobre la dirección y el país?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Lo mejor de todo es que está mostrando claramente en una cantidad mínima de sql qué columnas están asociadas con qué objeto. Dapper hace el resto.

BlackjacketMack
fuente
Este es un enfoque conciso siempre que ninguna tabla tenga campos de identificación.
Bernard Vander Beken
Con este enfoque, una tabla aún puede tener un campo Id ... pero debería ser el PK. Simplemente no tendría que crear el alias, por lo que en realidad es un poco menos de trabajo. (Creo que es muy inusual (¿mala forma?) Tener una columna llamada 'Id' que no sea el PK.)
BlackjacketMack
5

Suponiendo la siguiente estructura donde '|' es el punto de división y Ts son las entidades a las que se debe aplicar el mapeo.

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

A continuación se muestra la consulta elegante que tendrá que escribir.

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

Entonces queremos que TFirst asigne col_1 col_2 col_3, para TSecond el col_n col_m ...

La expresión splitOn se traduce en:

Comience a mapear todas las columnas en TFrist hasta que encuentre una columna denominada o con alias como 'col_3', y también incluya 'col_3' en el resultado del mapeo.

Luego comience a mapear en TSecond todas las columnas comenzando desde 'col_n' y continúe mapeando hasta que se encuentre un nuevo separador, que en este caso es 'col_A' y marca el inicio del mapeo TThird y así uno.

Las columnas de la consulta sql y los accesorios del objeto de mapeo están en una relación 1: 1 (lo que significa que deben tener el mismo nombre), si los nombres de las columnas resultantes de la consulta sql son diferentes, puede usar un alias con 'AS [ Some_Alias_Name] 'expresión.

boris
fuente
2

Hay una salvedad más. Si el campo CustomerId es nulo (normalmente en consultas con combinación izquierda), Dapper crea ProductItem con Customer = null. En el ejemplo anterior:

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 incluso una advertencia / trampa más. Si no asigna el campo especificado en splitOn y ese campo contiene nulo, Dapper crea y llena el objeto relacionado (Cliente en este caso). Para demostrar el uso de esta clase con sql anterior:

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
fuente
¿Hay una solución para el segundo ejemplo además de agregar el Customerid a la clase? Tengo un problema en el que necesito un objeto nulo, pero me está dando un objeto vacío. ( stackoverflow.com/questions/27231637/… )
jmzagorski
1

Hago esto de forma genérica en mi repositorio, funciona bien para mi caso de uso. Pensé en compartir. Quizás alguien amplíe esto más.

Algunos inconvenientes son:

  • Esto supone que las propiedades de su clave externa son el nombre de su objeto hijo + "Id", por ejemplo, UnitId.
  • Solo lo tengo mapeando 1 objeto secundario al padre.

El código:

    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
fuente
0

Si necesita mapear una entidad grande, escriba cada campo debe ser una tarea difícil.

Probé la respuesta de @BlackjacketMack, pero una de mis tablas tiene una columna de identificación, otras no (sé que es un problema de diseño de base de datos, pero ...) luego esto inserta una división adicional en dapper, por eso

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

No funciona para mi. Entonces terminé con un pequeño cambio de esto, basta con insertar un punto de división con un nombre que no coincide con ninguna de campo en las mesas, en mayo de casos cambiado as Idpor as _SplitPoint_, las miradas finales de secuencia de comandos SQL como la siguiente:

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

Luego, en apuesto, agregue solo una división como esta

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 Gómez
fuente