多重映射在 Dapper 中的正确使用

我正在尝试使用 Dapper 的多重映射特性来返回 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。我是否误解了多映射特性的核心功能?我不想每次都要添加一个完整的列名列表。

111907 次浏览

我刚刚做了一个很好的测试:

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将导致客户名为空。

如果将 CustomerId,CustomerName指定为分割点,则 dapper 假设您试图将结果集分割成3个对象。第一个从头开始,第二个从 CustomerId开始,第三个从 CustomerName开始。

还有一个警告。如果 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");

我通常在我的回购中这样做,对我的用例很有用。我想分享一下。也许有人会进一步扩大这个范围。

有些缺点是:

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

密码:

    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 了。

假设以下结构,其中’|’是分割点,T 是应该应用映射的实体。

       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 映射到 col1 col2 col3,TSecond 映射到 coln colm..。

SplitOn 表达式翻译过来就是:

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

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

SQL 查询的列和映射对象的道具是1:1关系(这意味着它们应该命名相同)。如果 SQL 查询产生的列名不同,则可以使用“ AS [ Some _ Alias _ Name ]”表达式为其命名。

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

我尝试了@BlackjacketMack 的回答,但是我的一个表有一个 Id 列,其他的没有(我知道这是一个 DB 设计问题,但是...) ,然后这个插入了一个额外的分裂在 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.*

然后再加一个这样的分裂

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

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

进场1

Entity Customer : Id Name

你的问题应该是这样的:

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

然后您的映射理解 Entity 和表之间的关系。

进场二

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

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