在Dapper中正确使用Multimapping

时间:2011-09-19 13:55:41

标签: dapper

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

[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。我想念 - 了解多重映射功能的核心功能。我不想每次都要添加完整的列名列表。

7 个答案:

答案 0 :(得分:159)

我刚刚运行了一项运行良好的测试:

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。如果有多个分割点,则需要将它们添加到逗号分隔列表中。

说你的记录集如下:

ProductID | ProductName | AccountOpened | CustomerId | CustomerName 
---------------------------------------   -------------------------

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

如果基础表中的列排序由于某种原因被翻转,那么这里有一个警告:

ProductID | ProductName | AccountOpened | CustomerName | CustomerId  
---------------------------------------   -------------------------

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

如果将CustomerId,CustomerName指定为分割点,则dapper假定您尝试将结果集拆分为3个对象。首先从开头开始,第二次从CustomerId开始,第三次在CustomerName开始。

答案 1 :(得分:22)

我们的表格与您的表格类似,其中包括&#34; CustomerID&#34;可能会使用&#39; select *&#39;操作。因此,Dapper正在完成它的工作,但只是过早地(可能)分裂,因为列将是:

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

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

我们做了什么,而且它在很多年里对数千个查询都很有效,只是使用Id的别名,而且从不指定spliton(使用Dapper&#39; Id&#39; Id&# 39。)

select 
p.*,

c.CustomerID AS Id,
c.*

...瞧!默认情况下,Dapper将仅在Id上拆分,并且Id在所有Customer列之前发生。当然,它会为返回的结果集添加一个额外的列,但这对于确切知道哪些列属于哪个对象的附加实用程序来说是非常小的开销。你可以轻松扩展它。需要地址和国家/地区信息吗?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

最重要的是,您在最少量的sql中清楚地显示哪些列与哪个对象相关联。 Dapper完成其余的工作。

答案 2 :(得分:3)

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

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

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

对我不起作用。然后我对此做了一点更改,只需插入一个名称与表上任何字段都不匹配的分割点,在可能的情况下,as Idas _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();

答案 3 :(得分: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中指定的字段并且该字段包含null,则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");  

答案 4 :(得分: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;
    }

答案 5 :(得分:1)

假定以下sql查询结构(列名表示形式,值无关)

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

因此,在dapper中,您将使用以下查询(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")

我们希望TFirst在其中映射第一部分TSecond 2nd等。

splitOn表达式转换为:

将所有列映射到TFrist,直到找到名为或别名为“ col_3”的列,并将该列也包含在映射中。

然后从col_n到结束映射到TSecond或找到新的分隔符(也将其包含在映射col_n中)

然后映射到TThird,从col_A开始直到结束或找到新的分隔符(也将其包含在映射col_A中)

然后从col_9开始映射到TFourth直到结束或找到新的分隔符(也将其包含在映射col_9中)

sql查询的列与映射对象的prop处于1:1关系(意味着它们应命名为相同),如果sql查询产生的列名称不同,则将使用别名AS [ Some_Alias_Name]

答案 6 :(得分:0)

我要注意一个非常重要的方面:实体中的属性名称必须与 select 语句匹配splitOn 的另一个方面是默认情况下它如何查找 Id,因此您不必指定它,除非您的命名类似于 CustomerId,而不是 Id。让我们看看这两种方法:

方法一

Entity Customer : Id Name

您的查询应该类似于:

SELECT c.Id as nameof{Customer.Id}, c.Foo As nameof{Customer.Name}.

然后你的映射理解实体和表之间的关系。

方法 2

实体客户:CustomerId、FancyName 选择 c.Id 作为 nameof{Customer.CustomerId},c.WeirdAssName 作为 nameof{Customer.FancyName} 并且在映射结束时,您必须使用 SplitOn 指定 Id 是 CustomerId。

我遇到了一个问题,即使由于与 SQL 语句不匹配,映射在技术上是正确的,我也没有得到我的值。