129

我正在尝试使用 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; }
}

我的简洁代码:

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

4

7 回答 7

222

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

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

假设您的记录集如下所示:

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

Dapper 需要知道如何按此顺序将列拆分为 2 个对象。粗略地看一下,Customer 从列开始CustomerId,因此splitOn: CustomerId

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

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

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

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

于 2011-09-20T00:50:57.750 回答
33

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

select
p.*,

c.CustomerID AS Id,
c.*,

address.AddressID AS Id,
address.*,

country.CountryID AS Id,
country.*

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

于 2017-08-05T22:57:28.500 回答
13

假设以下结构,其中 '|' 是拆分点,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 表达式转换为:

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

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

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

于 2019-03-07T05:40:35.993 回答
4

还有一个警告。如果 CustomerId 字段为空(通常在使用左连接的查询中),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");  
于 2013-04-19T15:18:31.160 回答
4

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

我尝试了@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();
于 2020-05-30T01:16:45.460 回答
3

我通常在我的回购中这样做,对我的用例很有用。我以为我会分享。也许有人会进一步扩展。

一些缺点是:

  • 这假设您的外键属性是您的子对象的名称 +“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;
    }
于 2016-04-27T21:06:47.840 回答
0

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

方法一

Entity Customer : Id Name

您的查询应该是这样的:

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

然后您的映射了解实体和表之间的关系。

方法二

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

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

于 2021-06-29T11:08:21.277 回答