在 Dapper 中正确使用多重映射

Correct use of multimapping in Dapper

提问人:Richard Forrest 提问时间:9/19/2011 最后编辑:PangRichard Forrest 更新时间:11/1/2023 访问量:123735

问:

我正在尝试使用 Dapper 的多映射功能来返回 ProductItems 和相关客户的列表。

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

我的 Dapper 代码:

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

C# Dapper

评论


答:

234赞 Sam Saffron 9/20/2011 #1

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

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 个对象。粗略地看一下,就会发现 Customer 从列开始,因此 。CustomerIdsplitOn: CustomerId

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

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

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

如果指定为分割点,dapper 假定您尝试将结果集拆分为 3 个对象。第一个从开始,第二个从开始,第三个从。CustomerId,CustomerNameCustomerIdCustomerName

评论

4赞 Richard Forrest 9/20/2011
谢谢山姆。是的,你说得对,列的返回顺序是 CustomerName |返回的 CustomerId:CustomerName 返回 null。
46赞 jes 8/30/2013
要记住的一件事是,你不能在 中包含空格,即 not ,因为 Dapper 不会显示字符串拆分的结果。它只会抛出通用的spliton错误。有一天把我逼疯了。splitonCustomerId,CustomerNameCustomerId, CustomerNameTrim
4赞 Harag 5/26/2017
@vaheeds您应该始终使用列名而永远不要使用星号,这样可以减少 sql 的工作量,并且您不会遇到列顺序错误的情况,就像在这种情况下一样。
6赞 Harag 5/26/2017
@vaheeds - 关于 id、Id、ID 查看 dapper 代码,它不区分大小写,并且它还修剪了 splitOn 的文本 - 这是 dapper 的 v1.50.2.0。
5赞 Sbu 11/9/2017
对于任何想知道的人,如果您必须将查询拆分为 3 个对象:在一个名为“Id”的列和一个名为“somethingId”的列上,请确保在拆分子句中包含第一个“Id”。尽管 Dapper 默认在“Id”上拆分,但在这种情况下,必须显式设置它。
4赞 Frantisek Bachan 4/19/2013 #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");  
3赞 Dylan Hayes 4/28/2016 #3

我通常在我的 repo 中执行此操作,适用于我的用例。我想我会分享。也许有人会进一步扩展这一点。

一些缺点是:

  • 这假定您的外键属性是子对象的名称 + “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;
    }
37赞 BlackjacketMack 8/6/2017 #4

我们的表的命名与您的表类似,其中类似“CustomerID”的内容可能会使用“select *”操作返回两次。因此,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 出现在所有 Customer 列之前。当然,它会在返回结果集中添加一个额外的列,但对于确切知道哪些列属于哪个对象的额外实用程序来说,这是非常小的开销。您可以轻松扩展它。需要地址和国家/地区信息?

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

最重要的是,您可以在最少量的 SQL 中清楚地显示哪些列与哪个对象相关联。Dapper 会完成剩下的工作。

评论

0赞 Bernard Vander Beken 1/20/2020
只要没有表具有 Id 字段,这是一种简洁的方法。
0赞 BlackjacketMack 1/21/2020
使用这种方法,表仍然可以有一个 Id 字段...但应该是PK。您只需要创建别名,因此实际上工作量会少一些。(我认为有一个名为“Id”的列不是 PK 是非常不寻常的(糟糕的形式?)。
0赞 Brandon Kramer 4/12/2022
您也可以轻松地将“Id”替换为任何其他列名,在这种情况下,您只需要有一个 splitOn 参数。由于我继承了一个数据库,其中有人确实使用 Id 作为非主键列的名称,因此我最终使用 _ 代替了 Id。
25赞 user9124444 3/7/2019 #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, TResult> 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 表达式转换为:

开始将所有列映射到 TFirst,直到找到名为“col_3”或别名为“”的列,并将“col_3”包含在映射结果中。

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

SQL 查询的列和映射对象的 props 是 1:1 的关系(这意味着它们应该以相同的方式命名)。如果 SQL 查询生成的列名不同,则可以使用“AS [Some_Alias_Name]”表达式为它们设置别名。

评论

1赞 Patrick Tucci 9/20/2022
这是此线程中关于此功能工作原理的最佳视觉解释。
5赞 Juan Pablo Gomez 5/30/2020 #6

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

我尝试@BlackjacketMack答案,但是我的一个表有一个 Id 列,其他表没有(我知道这是一个数据库设计问题,但是...... 然后这会在 dapper 上插入一个额外的拆分,这就是原因

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

对我不起作用。然后我对此进行了一些更改,只需插入一个名称与表上的任何字段都不匹配的拆分点,在可能更改的情况下,最终的 sql 脚本如下所示:as Idas _SplitPoint_

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

评论

1赞 drizin 10/10/2023
我从来没有想过使用相同的名称,但我通常会做一些与你非常相似的事情,只是我不使用真正的列。我确实喜欢SELECT t1.*, NULL as ColSplitter1, t2.*, NULL as ColSplitter2, t3.* FROM ...
0赞 alin 6/29/2021 #7

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

方法 1

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} 在映射结束时,必须使用 指定 Id 是 CustomerId。SplitOn

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