Uso correto de Multimapping no Dapper

111

Estou tentando usar o recurso Multimapping do dapper para retornar uma lista de ProductItems e clientes associados.

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

Meu código dapper é o seguinte

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

Isso funciona bem, mas parece que tenho que adicionar a lista de colunas completa ao parâmetro splitOn para retornar todas as propriedades dos clientes. Se eu não adicionar "CustomerName", ele retornará nulo. Estou entendendo mal a funcionalidade central do recurso de multimapeamento. Não quero ter que adicionar uma lista completa de nomes de coluna todas as vezes.

Richard Forrest
fonte
como vc realmente mostrar ambas as tabelas no datagridview entao? um pequeno exemplo será muito apreciado.
Ankur Soni

Respostas:

184

Acabei de executar um teste que funciona bem:

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

O parâmetro splitOn precisa ser especificado como o ponto de divisão, o padrão é Id. Se houver vários pontos de divisão, você precisará adicioná-los em uma lista delimitada por vírgulas.

Digamos que seu conjunto de registros seja assim:

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

O Dapper precisa saber como dividir as colunas nesta ordem em 2 objetos. Uma olhada rápida mostra que o cliente começa na coluna CustomerId, portanto splitOn: CustomerId.

Há uma grande advertência aqui, se a ordem das colunas na tabela subjacente for invertida por algum motivo:

ProductID | ProductName | AccountOpened | CustomerName | Identificação do Cliente  
--------------------------------------- ----------- --------------

splitOn: CustomerId resultará em um nome de cliente nulo.

Se você especificar CustomerId,CustomerNamecomo pontos de divisão, o dapper assume que você está tentando dividir o conjunto de resultados em 3 objetos. Primeiro começa no início, segundo começa em CustomerId, terceiro em CustomerName.

Sam Saffron
fonte
2
Obrigado Sam. Sim, você está certo, era a ordem de retorno das colunas que era o problema com CustomerName | CustomerId sendo retornado CustomerName estava voltando nulo.
Richard Forrest de
18
Uma coisa a lembrar é que você não pode ter espaços no spliton, ou seja , CustomerId,CustomerNamenão CustomerId, CustomerName, já que o Dapper não Trimdivide os resultados da string. Ele apenas lançará o erro spliton genérico. Um dia me deixou louco.
jes
2
@vaheeds você deve SEMPRE usar nomes de colunas e nunca uma estrela, isso dá ao sql menos trabalho para fazer e você não obtém situações em que a ordem das colunas está errada, como neste caso.
Harag
3
@vaheeds - em relação ao id, Id, ID olhando para o código dapper, ele não diferencia maiúsculas de minúsculas e também corta o texto para o splitOn - é v1.50.2.0 de dapper.
Harag
2
Para quem está se perguntando, caso você precise dividir uma consulta em 3 objetos: em uma coluna chamada "Id" e em uma coluna chamada "somethingId", certifique-se de incluir o primeiro "Id" na cláusula de divisão. Mesmo que o Dapper se divida por padrão em "Id", neste caso ele deve ser definido explicitamente.
Sbu
27

Nossas tabelas são nomeadas de forma semelhante à sua, onde algo como "CustomerID" pode ser retornado duas vezes usando uma operação 'select *'. Portanto, Dapper está fazendo seu trabalho, mas apenas dividindo muito cedo (possivelmente), porque as colunas seriam:

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

Isso torna o parâmetro spliton: não tão útil, especialmente quando você não tem certeza da ordem em que as colunas são retornadas. Claro que você pode especificar colunas manualmente ... mas é 2017 e raramente fazemos isso mais para obter objetos básicos.

O que fazemos, e funcionou muito bem para milhares de consultas por muitos anos, é simplesmente usar um alias para Id e nunca especificar spliton (usando o 'Id' padrão de Dapper).

select 
p.*,

c.CustomerID AS Id,
c.*

... voila! O Dapper só será dividido no Id por padrão, e esse Id ocorre antes de todas as colunas do Cliente. É claro que isso adicionará uma coluna extra ao conjunto de resultados de retorno, mas isso é uma sobrecarga extremamente mínima para a utilidade adicional de saber exatamente quais colunas pertencem a qual objeto. E você pode facilmente expandir isso. Precisa de informações de endereço e país?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

O melhor de tudo é que você está mostrando claramente em uma quantidade mínima de sql quais colunas estão associadas a quais objetos. Dapper faz o resto.

BlackjacketMack
fonte
Esta é uma abordagem concisa, desde que nenhuma tabela tenha campos de Id.
Bernard Vander Beken
Com esta abordagem, uma tabela ainda pode ter um campo Id ... mas deve ser o PK. Você simplesmente não teria que criar o alias, então é realmente um pouco menos trabalhoso. (Eu acho muito incomum (má forma?) Ter uma coluna chamada 'Id' que não seja o PK.)
BlackjacketMack
5

Assumindo a seguinte estrutura onde '|' é o ponto de divisão e Ts são as entidades às quais o mapeamento deve ser aplicado.

       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 seguir está a consulta dapper que você terá que escrever.

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

Então, queremos que TFirst mapeie col_1 col_2 col_3, para TSegundo o col_n col_m ...

A expressão splitOn se traduz em:

Comece o mapeamento de todas as colunas no TFrist até encontrar uma coluna com o nome ou alias de 'col_3', e também inclua 'col_3' no resultado do mapeamento.

Então comece a mapear em TSegundo todas as colunas começando de 'col_n' e continue mapeando até que um novo separador seja encontrado, que neste caso é 'col_A' e marca o início do terceiro mapeamento e assim um.

As colunas da consulta sql e os props do objeto de mapeamento estão em uma relação 1: 1 (o que significa que devem ter o mesmo nome), se os nomes das colunas resultantes da consulta sql forem diferentes, você pode usar o apelido 'AS [ Expressão Some_Alias_Name] '.

Boris
fonte
2

Há mais uma advertência. Se o campo CustomerId for nulo (geralmente em consultas com junção à esquerda), Dapper cria ProductItem com Customer = null. No exemplo acima:

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 ainda mais uma advertência / armadilha. Se você não mapear o campo especificado em splitOn e esse campo contiver nulo, o Dapper cria e preenche o objeto relacionado (Cliente, neste caso). Para demonstrar o uso desta classe com 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
fonte
existe uma solução para o segundo exemplo além de adicionar o Customerid à classe? Estou tendo um problema em que preciso de um objeto nulo, mas ele está me fornecendo um objeto vazio. ( stackoverflow.com/questions/27231637/… )
jmzagorski 01 de
1

Eu faço isso genericamente em meu repo, funciona bem para meu caso de uso. Eu pensei em compartilhar. Talvez alguém vá estender isso ainda mais.

Algumas desvantagens são:

  • Isso assume que suas propriedades de chave estrangeira são o nome de seu objeto filho + "Id", por exemplo, UnitId.
  • Eu tenho apenas mapeando 1 objeto filho para o pai.

O 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
fonte
0

Se você precisa mapear uma grande entidade, cada campo deve ser uma tarefa difícil.

Eu tentei a resposta @BlackjacketMack, mas uma das minhas tabelas tem uma coluna Id outras não (eu sei que é um problema de design de banco de dados, mas ...) então isso insere uma divisão extra no dapper, é por isso

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

Não funciona para mim. Então eu terminei com uma pequena mudança nisso, apenas insira um ponto de divisão com um nome que não corresponda a nenhum campo nas tabelas, no caso alterado as Idpor as _SplitPoint_, o script sql final fica assim:

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

Então, no dapper, adicione apenas um splitOn como este

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
fonte