提问人:Richard Forrest 提问时间:9/19/2011 最后编辑:PangRichard Forrest 更新时间:11/1/2023 访问量:123735
在 Dapper 中正确使用多重映射
Correct use of multimapping in Dapper
问:
我正在尝试使用 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。我是否误解了多映射功能的核心功能?我不想每次都添加完整的列名列表。
答:
我刚刚运行了一个运行良好的测试:
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 从列开始,因此 。CustomerId
splitOn: CustomerId
这里有一个很大的警告,如果基础表中的列顺序由于某种原因被翻转:
ProductID | ProductName | AccountOpened | CustomerName | CustomerId --------------------------------------- -------------------------
splitOn: CustomerId
将导致客户名称为 null。
如果指定为分割点,dapper 假定您尝试将结果集拆分为 3 个对象。第一个从开始,第二个从开始,第三个从。CustomerId,CustomerName
CustomerId
CustomerName
评论
spliton
CustomerId,CustomerName
CustomerId, CustomerName
Trim
还有一点需要注意。如果 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");
我通常在我的 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;
}
我们的表的命名与您的表类似,其中类似“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 会完成剩下的工作。
评论
假设采用以下结构,其中“|”是拆分点,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]”表达式为它们设置别名。
评论
如果需要映射一个大型实体,写入每个字段一定是一项艰巨的任务。
我尝试@BlackjacketMack答案,但是我的一个表有一个 Id 列,其他表没有(我知道这是一个数据库设计问题,但是...... 然后这会在 dapper 上插入一个额外的拆分,这就是原因
select
p.*,
c.CustomerID AS Id,
c.*,
address.AddressID AS Id,
address.*,
country.CountryID AS Id,
country.*
对我不起作用。然后我对此进行了一些更改,只需插入一个名称与表上的任何字段都不匹配的拆分点,在可能更改的情况下,最终的 sql 脚本如下所示:as Id
as _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();
评论
SELECT t1.*, NULL as ColSplitter1, t2.*, NULL as ColSplitter2, t3.* FROM ...
我想指出一个非常重要的方面:实体中的属性名称必须与 select 语句匹配。另一个方面是默认情况下它如何查找 Id,因此您不必指定它,除非您的命名类似于 ,而不是 .让我们看一下这 2 种方法:splitOn
CustomerId
Id
方法 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语句不匹配,映射在技术上是正确的,我也没有得到我的值。
评论