Dapper - 通过构造函数处理具有只读字段的 ddd 实体的自定义映射
Dapper - Handling custom mapping for ddd entity with read-only fields via constructor
我有以下 class 我正在尝试补充水分:
public class Product
{
public readonly Sku Sku;
public string Name { get; private set; }
public string Description { get; private set; }
public bool IsArchived { get; private set; }
public Product(Sku sku, string name, string description, bool isArchived)
{
Sku = sku;
Name = name;
Description = description;
IsArchived = isArchived;
}
}
它使用以下 classes 来实现我的 DDD 实体域模型中的概念(删除了不相关的代码以保持代码简短,设置为只读以使构造后不可变):
public class Sku
{
public readonly VendorId VendorId;
public readonly string SkuValue;
public Sku(VendorId vendorId, string skuValue)
{
VendorId = vendorId;
SkuValue = skuValue;
}
}
public class VendorId
{
public readonly string VendorShortname;
public VendorId(string vendorShortname)
{
VendorShortname = vendorShortname;
}
}
我尝试 运行 参数化查询,它将合并到产品对象中:
using (connection)
{
connection.Open();
return connection.QueryFirst<Product>(ReadQuery, new { VendorId = sku.VendorId.VendorShortname, SkuValue = sku.SkuValue });
}
它抛出以下异常,因为它不知道如何处理构造函数中的 Sku
类型:
System.InvalidOperationException: 'A parameterless default constructor
or one matching signature (System.String VendorId, System.String
SkuValue, System.String Name, System.String Description, System.UInt64
IsArchived) is required for Domain.Model.Products.Product
materialization'
我研究过使用自定义 SqlMapper.TypeHandler<Product>
,但 Parse(object value)
只从 VendorId
数据库列中传入单个解析值(如果它在此处传入值数组我可以自己做映射)。
有没有办法自定义对象的处理,以便我可以将所有参数传递给构造函数,如下所示:
using (connection)
{
var command = connection.CreateCommand();
command.CommandText = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
command.Parameters.AddWithValue("@VendorShortname", sku.VendorId.VendorShortname);
command.Parameters.AddWithValue("@SkuValue", sku.SkuValue);
connection.Open();
var reader = command.ExecuteReader();
if (reader.HasRows==false)
return null;
reader.Read();
return new Product(
new Sku(new VendorId(reader.GetString("VendorId")),reader.GetString("SkuValue")),
reader.GetString("Name"),
reader.GetString("Description"),
reader.GetBoolean("IsArchived"));
}
我想我可以用 Product(string VendorShortname, string SkuValue, string Name, string Description, UInt64 IsArchived)
创建一个特定的构造函数,但我宁愿(必须)在映射代码中而不是在我的域模型中考虑这个问题。
通过一些伪代码,我可以做的是推出我自己的 ORM,但我想通过 Dapper 做类似的事情。
- 通过反射获取对象的所有构造函数
- 如果构造函数中的任何参数是类型,则获取其构造函数
- 对于每个构造函数(包括参数),将构造函数名称映射到 SQL reader 列(和类型)
这相当于 VendorShortname
用于 VendorId(string vendorShortname)
,Name
、Description
、isArchived
用于 public Product(Sku sku, string name, string description, bool isArchived)
...根据我在下面 link 上发布的回答,MongoDB 也做了类似的事情,一个 Dapper 手动映射等效项会很棒
Execute a query and map it to a list of dynamic objects
public static IEnumerable<dynamic> Query (
this IDbConnection cnn,
string sql,
object param = null,
SqlTransaction transaction = null,
bool buffered = true
)
然后您将使用动态对象列表构建所需的模型。
因此使用原始 post 中的示例,参数化查询将从...
using (connection)
{
var command = connection.CreateCommand();
command.CommandText = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
command.Parameters.AddWithValue("@VendorShortname", sku.VendorId.VendorShortname);
command.Parameters.AddWithValue("@SkuValue", sku.SkuValue);
connection.Open();
var reader = command.ExecuteReader();
if (reader.HasRows==false)
return null;
reader.Read();
return new Product(
new Sku(new VendorId(reader.GetString("VendorId")),reader.GetString("SkuValue")),
reader.GetString("Name"),
reader.GetString("Description"),
reader.GetBoolean("IsArchived"));
}
到...
var ReadQuery = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
using (connection) {
connection.Open();
return connection.Query(ReadQuery, new { VendorShortname = sku.VendorId.VendorShortname, SkuValue = sku.SkuValue })
.Select(row => new Product(
new Sku(new VendorId(row.VendorShortname), row.SkuValue),
row.Name,
row.Description,
row.IsArchived)
);
}
这是框架的预期目的。只需确保使用的属性直接映射到查询返回的字段即可。
这可能看起来很复杂,但考虑到目标对象的构造函数的复杂性,这是一个可行的解决方案。
您还可以考虑将 "domain" 模型与持久性分离并在其他地方构建它们的选项。例如:
- 为每个数据库记录创建 class:ProductRecord
- 创建工厂:ProductFactory
- 获取数据:
var productRecords = connection.Query<ProductRecord>("select * from products").AsList();
- 构建产品:
factory.Build(productRecords)
一些优点:关注点分离、灵活性、适合大型项目
一些缺点:更多的代码,对小项目来说有点过头了
我有以下 class 我正在尝试补充水分:
public class Product
{
public readonly Sku Sku;
public string Name { get; private set; }
public string Description { get; private set; }
public bool IsArchived { get; private set; }
public Product(Sku sku, string name, string description, bool isArchived)
{
Sku = sku;
Name = name;
Description = description;
IsArchived = isArchived;
}
}
它使用以下 classes 来实现我的 DDD 实体域模型中的概念(删除了不相关的代码以保持代码简短,设置为只读以使构造后不可变):
public class Sku
{
public readonly VendorId VendorId;
public readonly string SkuValue;
public Sku(VendorId vendorId, string skuValue)
{
VendorId = vendorId;
SkuValue = skuValue;
}
}
public class VendorId
{
public readonly string VendorShortname;
public VendorId(string vendorShortname)
{
VendorShortname = vendorShortname;
}
}
我尝试 运行 参数化查询,它将合并到产品对象中:
using (connection)
{
connection.Open();
return connection.QueryFirst<Product>(ReadQuery, new { VendorId = sku.VendorId.VendorShortname, SkuValue = sku.SkuValue });
}
它抛出以下异常,因为它不知道如何处理构造函数中的 Sku
类型:
System.InvalidOperationException: 'A parameterless default constructor or one matching signature (System.String VendorId, System.String SkuValue, System.String Name, System.String Description, System.UInt64 IsArchived) is required for Domain.Model.Products.Product materialization'
我研究过使用自定义 SqlMapper.TypeHandler<Product>
,但 Parse(object value)
只从 VendorId
数据库列中传入单个解析值(如果它在此处传入值数组我可以自己做映射)。
有没有办法自定义对象的处理,以便我可以将所有参数传递给构造函数,如下所示:
using (connection)
{
var command = connection.CreateCommand();
command.CommandText = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
command.Parameters.AddWithValue("@VendorShortname", sku.VendorId.VendorShortname);
command.Parameters.AddWithValue("@SkuValue", sku.SkuValue);
connection.Open();
var reader = command.ExecuteReader();
if (reader.HasRows==false)
return null;
reader.Read();
return new Product(
new Sku(new VendorId(reader.GetString("VendorId")),reader.GetString("SkuValue")),
reader.GetString("Name"),
reader.GetString("Description"),
reader.GetBoolean("IsArchived"));
}
我想我可以用 Product(string VendorShortname, string SkuValue, string Name, string Description, UInt64 IsArchived)
创建一个特定的构造函数,但我宁愿(必须)在映射代码中而不是在我的域模型中考虑这个问题。
通过一些伪代码,我可以做的是推出我自己的 ORM,但我想通过 Dapper 做类似的事情。
- 通过反射获取对象的所有构造函数
- 如果构造函数中的任何参数是类型,则获取其构造函数
- 对于每个构造函数(包括参数),将构造函数名称映射到 SQL reader 列(和类型)
这相当于 VendorShortname
用于 VendorId(string vendorShortname)
,Name
、Description
、isArchived
用于 public Product(Sku sku, string name, string description, bool isArchived)
...根据我在下面 link 上发布的回答,MongoDB 也做了类似的事情,一个 Dapper 手动映射等效项会很棒
Execute a query and map it to a list of dynamic objects
public static IEnumerable<dynamic> Query ( this IDbConnection cnn, string sql, object param = null, SqlTransaction transaction = null, bool buffered = true )
然后您将使用动态对象列表构建所需的模型。
因此使用原始 post 中的示例,参数化查询将从...
using (connection)
{
var command = connection.CreateCommand();
command.CommandText = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
command.Parameters.AddWithValue("@VendorShortname", sku.VendorId.VendorShortname);
command.Parameters.AddWithValue("@SkuValue", sku.SkuValue);
connection.Open();
var reader = command.ExecuteReader();
if (reader.HasRows==false)
return null;
reader.Read();
return new Product(
new Sku(new VendorId(reader.GetString("VendorId")),reader.GetString("SkuValue")),
reader.GetString("Name"),
reader.GetString("Description"),
reader.GetBoolean("IsArchived"));
}
到...
var ReadQuery = "SELECT VendorShortname, SkuValue, Name, Description, IsArchived FROM Products WHERE VendorShortname=@VendorShortname AND SkuValue=@SkuValue";
using (connection) {
connection.Open();
return connection.Query(ReadQuery, new { VendorShortname = sku.VendorId.VendorShortname, SkuValue = sku.SkuValue })
.Select(row => new Product(
new Sku(new VendorId(row.VendorShortname), row.SkuValue),
row.Name,
row.Description,
row.IsArchived)
);
}
这是框架的预期目的。只需确保使用的属性直接映射到查询返回的字段即可。
这可能看起来很复杂,但考虑到目标对象的构造函数的复杂性,这是一个可行的解决方案。
您还可以考虑将 "domain" 模型与持久性分离并在其他地方构建它们的选项。例如:
- 为每个数据库记录创建 class:ProductRecord
- 创建工厂:ProductFactory
- 获取数据:
var productRecords = connection.Query<ProductRecord>("select * from products").AsList();
- 构建产品:
factory.Build(productRecords)
一些优点:关注点分离、灵活性、适合大型项目
一些缺点:更多的代码,对小项目来说有点过头了