在Dapper中正确使用多重映射


111

我正在尝试使用dapper的Multimapping功能返回ProductItems和关联的Customer的列表。

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

我的小巧的代码如下

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

这可以正常工作,但是我似乎必须将完整的列列表添加到splitOn参数中以返回所有客户属性。如果我不添加“ CustomerName”,则返回null。我是否错过了多重映射功能的核心功能。我不想每次都添加一个完整的列名列表。


您实际上如何在datagridview中显示两个表?一个小例子将不胜感激。
索尼

Answers:


184

我只是运行了一个运行良好的测试:

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

需要将splitOn参数指定为分割点,默认为Id。如果有多个分割点,则需要将它们添加到以逗号分隔的列表中。

说您的记录集如下所示:

产品编号| 产品名称| 开户| 客户编号| 顾客姓名
--------------------------------------- ----------- --------------

Dapper需要知道如何按此顺序将列拆分为2个对象。粗略的外观表明Customer从列开始CustomerId,因此splitOn: CustomerId

如果由于某种原因而翻转了基础表中的列顺序,这里有一个很大的警告:

产品编号| 产品名称| 开户| 客户名称| 顾客ID  
--------------------------------------- ----------- --------------

splitOn: CustomerId 将导致客户名称为空。

如果指定CustomerId,CustomerName为分割点,则dapper假设您正在尝试将结果集分割为3个对象。第一开始于起点,第二开始于起点,第三开始CustomerIdCustomerName


2
谢谢山姆。是的,您的权利是CustomerName |的问题是列的退货顺序。返回的CustomerId客户名返回空值。
理查德·福雷斯特

18
有一点要记住的是,你不能有空格spliton,即CustomerId,CustomerName不是CustomerId, CustomerName,因为小巧玲珑的不Trim字符串分割的结果。它只会抛出一般的spliton错误。有一天让我疯狂。
2013年

2
@vaheeds您应该始终使用列名称,而永远不要使用星号,这使sql要做的工作更少,并且在这种情况下,您不会遇到列顺序错误的情况。
哈拉格'17

3
@vaheeds-关于id,Id,ID,查看dapper代码不区分大小写,它还会修剪splitOn的文本-这是dapper的v1.50.2.0。
哈拉格'17

2
对于任何想知道的人,万一您必须将查询拆分为3个对象:在名为“ Id”的一列和名为“ somethingId”的一列上,请确保在split子句中包括第一个“ Id”。即使Dapper默认在“ Id”上分割,在这种情况下也必须显式设置。
SBU

27

我们的表的命名方式与您的表类似,其中,使用“ select *”操作可能会两次返回“ CustomerID”之类的内容。因此,Dapper只是在做自己的工作,但分裂得太早了(可能),因为这些列是:

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

这使得spliton:参数不是那么有用,特别是当您不确定返回列的顺序时。当然,您可以手动指定列...但是现在是2017年,对于基本对象的获取我们很少这样做。

我们所做的,并且多年来在数千个查询中都非常有用,它只是对ID使用别名,而从不指定spliton(使用Dapper的默认“ Id”)。

select 
p.*,

c.CustomerID AS Id,
c.*

...瞧!Dapper默认情况下仅按ID拆分,该ID出现在所有“客户”列之前。当然,它将为您的返回结果集添加一个额外的列,但是对于所添加的实用程序来说,这是非常小的开销,因为它可以准确地知道哪些列属于哪个对象。您可以轻松扩展它。需要地址和国家/地区信息吗?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

最重要的是,您将以最少的sql清楚地显示与哪个对象关联的列。精打细算的其余部分。


只要没有表具有ID字段,这就是一种简洁的方法。
Bernard Vander Beken

通过这种方法,表仍然可以具有Id字段...但是它应该是PK。您将不必创建别名,因此实际上要少一些工作。(我认为拥有称为“ Id”的列而不是PK的情况非常不常见(格式错误)。)
BlackjacketMack

5

假设以下结构在“ |” 是分割点,Ts是应应用映射的实体。

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

以下是您必须编写的dapper查询。

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

所以我们想让TFirst映射col_1 col_2 col_3,对于TSecond col_n col_m ...

splitOn表达式可转换为:

开始将所有列映射到TFrist,直到找到名为或别名为“ col_3”的列,并且还将“ col_3”包括到映射结果中。

然后开始映射到TSecond中所有从'col_n'开始的列,并继续映射直到找到新的分隔符,在本例中为'col_A'并标记TThird映射的开始,依此类推。

sql查询的列与映射对象的props处于1:1关系(意味着它们应命名为相同),如果sql查询产生的列名不同,则可以使用'AS [ Some_Alias_Name]”表达式。


2

还有一个警告。如果CustomerId字段为null(通常在具有左联接的查询中),则Dapper使用Customer = null创建ProductItem。在上面的示例中:

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

甚至还有一个警告/陷阱。如果不映射,则在splitOn中指定的字段并且该字段包含空Dapper会创建并填充相关对象(在这种情况下为Customer)。为了演示如何将此类与以前的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");  

除了将Customerid添加到类之外,第二个示例还有解决方案吗?我遇到一个需要空对象的问题,但这给了我一个空对象。(stackoverflow.com/questions/27231637/...
jmzagorski

1

我通常在回购中执行此操作,对我的用例而言效果很好。我以为我会分享。也许有人会进一步扩大这个范围。

一些缺点是:

  • 假设您的外键属性是子对象的名称+“ Id”,例如UnitId。
  • 我只将1个子对象映射到父对象。

代码:

    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

如果需要映射大型实体,则每个字段必须是一项艰巨的任务。

我尝试了@BlackjacketMack答案,但是我的一个表没有另一个Id列(我知道这是数据库设计问题,但是...)然后在dapper上插入了额外的拆分,这就是为什么

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

对我不起作用。然后,我有一点改变这个结束,只需插入一个名字的分割点不匹配上表中的任何领域,可以改变的情况下as Id通过as _SplitPoint_,最终的sql脚本如下所示:

select
p.*,

c.CustomerID AS _SplitPoint_,
c.*,

address.AddressID AS _SplitPoint_,
address.*,

country.CountryID AS _SplitPoint_,
country.*

然后在dapper中仅添加一个splitOn

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();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.