我应该在相关的聚合根之间强制执行参照完整性吗?如果有,在哪里?
Should I enforce referential integrity between my related aggregate roots? If so, where?
这似乎是一个基本问题,我对 DDD 的研究不多。这个问题的背景是关于使用 ABP 框架、它的层和它生成的代码。
我应该在相关聚合根之间强制执行引用完整性吗?如果有,在哪里?
在使用ABP Suite 生成我的初始实体(其中很多是聚合根(AR))之后,我开始实现它们之间的导航属性。我最初的方法是修改每个 entity/AR 的构造函数以包含从属 entities/ARs.
的 Guid ID
原始方法
public Address(Guid id, string name, AddressTypes addressType,
string line1, string line2, string line3,
string city, string postalCode,
Guid stateId, Guid countryId, // <-- dependent AR IDs
bool primary = false, bool active = true)
{
//other properties set
StateId = stateId;
CountryId = countryId;
//etc
}
我让我的域数据种子逻辑与我的数据种子贡献者一起工作,然后我继续处理测试数据。
很快我意识到我必须创建每个依赖项 entities/ARs 并将它们的 ID 传递给每个被测实体的构造函数。
这让我在文档中搜索示例或最佳实践。我遇到的一件事是来自 Entity Best Practices & Conventions 页面的声明:
Do always reference to other aggregate roots by Id. Never add navigation properties to other aggregate roots.
好的。因此,这似乎表明我应该在我的 principal/parent AR 中为我的每个 dependent/child AR 设置可为 null 的 ID 属性。 AR 下的完全托管实体可能可以自由拥有导航属性,但 AR 不能。
因为我想在更高级别管理 AR 关系,所以我似乎应该从我的构造函数中删除相关的 AR,或者至少使它们可以为 null。
修改后的方法 1
public Address(Guid id, string name, AddressTypes addressType,
string line1, string line2, string line3,
string city, string postalCode,
Guid? stateId = null, Guid? countryId = null, // <-- nullable dependent AR IDs
bool primary = false, bool active = true)
{
//other properties set
StateId = stateId;
CountryId = countryId;
//etc
}
修改后的方法 2
public Address(Guid id, string name, AddressTypes addressType,
string line1, string line2, string line3,
string city, string postalCode,
bool primary = false, bool active = true)
{
//other properties set, not setting dependent IDs
}
显然修改后的方法 2 将要求应用程序服务在构造对象后设置依赖 ID 属性:
Address address = null;
address = await _addressRepository.InsertAsync(new Address
(
id: _guidGenerator.Create(),
name: "DEMO Contact Home Address",
addressType: AddressTypes.Home,
line1: "123 Main St",
line2: "",
line3: "",
city: "Anytown",
postalCode: "00000",
primary: true,
active: true
), autoSave: true);
address.StateId = alState.Id; // set here
address.CountryId = usId; // and here
到目前为止我走上正轨了吗?
所以,如果我想强制执行参照完整性,我不应该通过 Entity Framework(或选择的 ORM)来实现。我应该改为在应用程序服务层实施参照完整性。
[Authorize(MyProductPermissions.Addresses.Create)]
public virtual async Task<AddressDto> CreateAsync(AddressCreateDto input)
{
if (input.StateId == default)
{
throw new UserFriendlyException(L["The {0} field is required.", L["State"]]);
}
if (input.CountryId == default)
{
throw new UserFriendlyException(L["The {0} field is required.", L["Country"]]);
}
var address = ObjectMapper.Map<AddressCreateDto, Address>(input);
address.TenantId = CurrentTenant.Id;
// BEGIN Referential Integrity Logic
// Assumes that the dependent IDs will be set when the DTO is mapped to the entity
if (address.StateId == null)
{
// log and throw 500 error
}
if (address.CountryId == null)
{
// log and throw 500 error
}
// END Referential Integrity Logic
address = await _addressRepository.InsertAsync(address, autoSave: true);
return ObjectMapper.Map<Address, AddressDto>(address);
}
这样理解正确吗?
如果我的依赖 ID 可以为 null,我可以继续使用 ABP Suite 生成的测试代码。
await _addressRepository.InsertAsync(new Address
(
Guid.Parse("ca846f1a-8bbd-4e2c-afbd-8e40a03ae18f"),
"7d7b348e410d48ee89e1807beb2f2ac0bd66af4ea82943ec8eee3a52962577b1",
default,
"de5ec0226aba4c1a837c9716b21af6551d10436756724d4fa507028eaaddcdadec779bea0ef04922992f9d2432068b180e6fe95f425f47c68559c1dbd4360fdb",
"53bc12edeb4544158147f3b835b0c4ce5e581844f5c248d69647d80d398706f5ee1c769e4ee14bd0a1e776a369a96ea3c0582b659ce342bdbdf40e6668f3b9f9",
"117880188dfd4a6f96892fea3e62a16f057748ebe76b4dd0a4402918e2fee9055272ff81c53d4c28825cc20d01918386864efd54e1aa458bb449a1d12b349d40",
"866a81007219411a971be2133bf4b5882d4ef612722a45ac91420e0b30d774ed",
"93bba338449444f5",
true,
true
));
如果不是,我将不得不扩充测试代码,为与我的 AR 关联的每个依赖实体类型创建至少一个依赖实体。
// Create dependent State entity
var alState = //...
// Create dependent Country entity
var country = //...
Address address = null;
address = await _addressRepository.InsertAsync(new Address
(
Guid.Parse("ca846f1a-8bbd-4e2c-afbd-8e40a03ae18f"),
"7d7b348e410d48ee89e1807beb2f2ac0bd66af4ea82943ec8eee3a52962577b1",
default,
"de5ec0226aba4c1a837c9716b21af6551d10436756724d4fa507028eaaddcdadec779bea0ef04922992f9d2432068b180e6fe95f425f47c68559c1dbd4360fdb",
"53bc12edeb4544158147f3b835b0c4ce5e581844f5c248d69647d80d398706f5ee1c769e4ee14bd0a1e776a369a96ea3c0582b659ce342bdbdf40e6668f3b9f9",
"117880188dfd4a6f96892fea3e62a16f057748ebe76b4dd0a4402918e2fee9055272ff81c53d4c28825cc20d01918386864efd54e1aa458bb449a1d12b349d40",
"866a81007219411a971be2133bf4b5882d4ef612722a45ac91420e0b30d774ed",
"93bba338449444f5",
true,
true
));
address.StateId = state.Id;
address.CountryId = country.Id;
我的层次结构中有很多对象,目前大约有 30 个 entities/ARs。多级依赖性加剧了这种情况。
请帮助我了解 DDD 世界中的最佳实践。在开始实施 30 个奇怪的构造函数和应用程序服务之前,我需要做对这一点。
如果必须使用指定的 StateId
和 CountryId
创建您的 Address
实体,您需要使用原始方法并强制在创建对象时设置它们的值。因为,聚合根 负责维护自身的完整性。 (见相关
documentation 了解更多信息)
- 我猜你也在问如果
StateId
在你的数据库中不存在并且它只是一个简单的 GUID 会发生什么。在这种情况下,如果您将 StateId 设置为外键,它将不会添加到您的数据库中。但是如果你想以任何方式查询它并在它不存在时抛出异常,你可以创建一个域服务并检查是否存在具有给定 stateId 的状态,如果存在则将其传递给 Address 构造函数(如果不存在抛出异常)并在数据库中创建新的地址记录。
public class AddressManager : DomainService
{
private readonly IAddressRepository _addressRepository;
private readonly IStateRepository _stateRepository;
private readonly ICountryRepository _countryRepository;
public AddressManager(IAddressRepository addressRepository, IStateRepository stateRepository, ICountryRepository countryRepository)
{
_addressRepository = addressRepository;
_stateRepository = stateRepository;
_countryRepository = countryRepository;
}
public async Task CreateAsync(string name, AddressTypes addressType,
string line1, string line2, string line3,
string city, string postalCode,
Guid stateId, Guid countryId)
{
if(await _stateRepository.FindAsync(stateId))
{
//throw exception
return;
}
if(await _countryRepository.FindAsync(stateId))
{
//throw exception
return;
}
var address = new Address(GuidGenerator.Create(), AddressTypes.Typee, "line1", "line2", "line3", "city", "postalCode", stateId, countryId);
await _addressRepository.InsertAsync(address);
}
}
并且在您的应用服务中创建新地址时调用 AddressManager 的 CreateAsync 方法。 (您可能希望将 Address 实体构造函数设置为 internal 而不是 public 以防止在应用层错误地创建 Address 对象。)
这似乎是一个基本问题,我对 DDD 的研究不多。这个问题的背景是关于使用 ABP 框架、它的层和它生成的代码。
我应该在相关聚合根之间强制执行引用完整性吗?如果有,在哪里?
在使用ABP Suite 生成我的初始实体(其中很多是聚合根(AR))之后,我开始实现它们之间的导航属性。我最初的方法是修改每个 entity/AR 的构造函数以包含从属 entities/ARs.
的 Guid ID原始方法
public Address(Guid id, string name, AddressTypes addressType,
string line1, string line2, string line3,
string city, string postalCode,
Guid stateId, Guid countryId, // <-- dependent AR IDs
bool primary = false, bool active = true)
{
//other properties set
StateId = stateId;
CountryId = countryId;
//etc
}
我让我的域数据种子逻辑与我的数据种子贡献者一起工作,然后我继续处理测试数据。
很快我意识到我必须创建每个依赖项 entities/ARs 并将它们的 ID 传递给每个被测实体的构造函数。
这让我在文档中搜索示例或最佳实践。我遇到的一件事是来自 Entity Best Practices & Conventions 页面的声明:
Do always reference to other aggregate roots by Id. Never add navigation properties to other aggregate roots.
好的。因此,这似乎表明我应该在我的 principal/parent AR 中为我的每个 dependent/child AR 设置可为 null 的 ID 属性。 AR 下的完全托管实体可能可以自由拥有导航属性,但 AR 不能。
因为我想在更高级别管理 AR 关系,所以我似乎应该从我的构造函数中删除相关的 AR,或者至少使它们可以为 null。
修改后的方法 1
public Address(Guid id, string name, AddressTypes addressType,
string line1, string line2, string line3,
string city, string postalCode,
Guid? stateId = null, Guid? countryId = null, // <-- nullable dependent AR IDs
bool primary = false, bool active = true)
{
//other properties set
StateId = stateId;
CountryId = countryId;
//etc
}
修改后的方法 2
public Address(Guid id, string name, AddressTypes addressType,
string line1, string line2, string line3,
string city, string postalCode,
bool primary = false, bool active = true)
{
//other properties set, not setting dependent IDs
}
显然修改后的方法 2 将要求应用程序服务在构造对象后设置依赖 ID 属性:
Address address = null;
address = await _addressRepository.InsertAsync(new Address
(
id: _guidGenerator.Create(),
name: "DEMO Contact Home Address",
addressType: AddressTypes.Home,
line1: "123 Main St",
line2: "",
line3: "",
city: "Anytown",
postalCode: "00000",
primary: true,
active: true
), autoSave: true);
address.StateId = alState.Id; // set here
address.CountryId = usId; // and here
到目前为止我走上正轨了吗?
所以,如果我想强制执行参照完整性,我不应该通过 Entity Framework(或选择的 ORM)来实现。我应该改为在应用程序服务层实施参照完整性。
[Authorize(MyProductPermissions.Addresses.Create)]
public virtual async Task<AddressDto> CreateAsync(AddressCreateDto input)
{
if (input.StateId == default)
{
throw new UserFriendlyException(L["The {0} field is required.", L["State"]]);
}
if (input.CountryId == default)
{
throw new UserFriendlyException(L["The {0} field is required.", L["Country"]]);
}
var address = ObjectMapper.Map<AddressCreateDto, Address>(input);
address.TenantId = CurrentTenant.Id;
// BEGIN Referential Integrity Logic
// Assumes that the dependent IDs will be set when the DTO is mapped to the entity
if (address.StateId == null)
{
// log and throw 500 error
}
if (address.CountryId == null)
{
// log and throw 500 error
}
// END Referential Integrity Logic
address = await _addressRepository.InsertAsync(address, autoSave: true);
return ObjectMapper.Map<Address, AddressDto>(address);
}
这样理解正确吗?
如果我的依赖 ID 可以为 null,我可以继续使用 ABP Suite 生成的测试代码。
await _addressRepository.InsertAsync(new Address
(
Guid.Parse("ca846f1a-8bbd-4e2c-afbd-8e40a03ae18f"),
"7d7b348e410d48ee89e1807beb2f2ac0bd66af4ea82943ec8eee3a52962577b1",
default,
"de5ec0226aba4c1a837c9716b21af6551d10436756724d4fa507028eaaddcdadec779bea0ef04922992f9d2432068b180e6fe95f425f47c68559c1dbd4360fdb",
"53bc12edeb4544158147f3b835b0c4ce5e581844f5c248d69647d80d398706f5ee1c769e4ee14bd0a1e776a369a96ea3c0582b659ce342bdbdf40e6668f3b9f9",
"117880188dfd4a6f96892fea3e62a16f057748ebe76b4dd0a4402918e2fee9055272ff81c53d4c28825cc20d01918386864efd54e1aa458bb449a1d12b349d40",
"866a81007219411a971be2133bf4b5882d4ef612722a45ac91420e0b30d774ed",
"93bba338449444f5",
true,
true
));
如果不是,我将不得不扩充测试代码,为与我的 AR 关联的每个依赖实体类型创建至少一个依赖实体。
// Create dependent State entity
var alState = //...
// Create dependent Country entity
var country = //...
Address address = null;
address = await _addressRepository.InsertAsync(new Address
(
Guid.Parse("ca846f1a-8bbd-4e2c-afbd-8e40a03ae18f"),
"7d7b348e410d48ee89e1807beb2f2ac0bd66af4ea82943ec8eee3a52962577b1",
default,
"de5ec0226aba4c1a837c9716b21af6551d10436756724d4fa507028eaaddcdadec779bea0ef04922992f9d2432068b180e6fe95f425f47c68559c1dbd4360fdb",
"53bc12edeb4544158147f3b835b0c4ce5e581844f5c248d69647d80d398706f5ee1c769e4ee14bd0a1e776a369a96ea3c0582b659ce342bdbdf40e6668f3b9f9",
"117880188dfd4a6f96892fea3e62a16f057748ebe76b4dd0a4402918e2fee9055272ff81c53d4c28825cc20d01918386864efd54e1aa458bb449a1d12b349d40",
"866a81007219411a971be2133bf4b5882d4ef612722a45ac91420e0b30d774ed",
"93bba338449444f5",
true,
true
));
address.StateId = state.Id;
address.CountryId = country.Id;
我的层次结构中有很多对象,目前大约有 30 个 entities/ARs。多级依赖性加剧了这种情况。
请帮助我了解 DDD 世界中的最佳实践。在开始实施 30 个奇怪的构造函数和应用程序服务之前,我需要做对这一点。
如果必须使用指定的 StateId
和 CountryId
创建您的 Address
实体,您需要使用原始方法并强制在创建对象时设置它们的值。因为,聚合根 负责维护自身的完整性。 (见相关
documentation 了解更多信息)
- 我猜你也在问如果
StateId
在你的数据库中不存在并且它只是一个简单的 GUID 会发生什么。在这种情况下,如果您将 StateId 设置为外键,它将不会添加到您的数据库中。但是如果你想以任何方式查询它并在它不存在时抛出异常,你可以创建一个域服务并检查是否存在具有给定 stateId 的状态,如果存在则将其传递给 Address 构造函数(如果不存在抛出异常)并在数据库中创建新的地址记录。
public class AddressManager : DomainService
{
private readonly IAddressRepository _addressRepository;
private readonly IStateRepository _stateRepository;
private readonly ICountryRepository _countryRepository;
public AddressManager(IAddressRepository addressRepository, IStateRepository stateRepository, ICountryRepository countryRepository)
{
_addressRepository = addressRepository;
_stateRepository = stateRepository;
_countryRepository = countryRepository;
}
public async Task CreateAsync(string name, AddressTypes addressType,
string line1, string line2, string line3,
string city, string postalCode,
Guid stateId, Guid countryId)
{
if(await _stateRepository.FindAsync(stateId))
{
//throw exception
return;
}
if(await _countryRepository.FindAsync(stateId))
{
//throw exception
return;
}
var address = new Address(GuidGenerator.Create(), AddressTypes.Typee, "line1", "line2", "line3", "city", "postalCode", stateId, countryId);
await _addressRepository.InsertAsync(address);
}
}
并且在您的应用服务中创建新地址时调用 AddressManager 的 CreateAsync 方法。 (您可能希望将 Address 实体构造函数设置为 internal 而不是 public 以防止在应用层错误地创建 Address 对象。)