我应该在相关的聚合根之间强制执行参照完整性吗?如果有,在哪里?

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 个奇怪的构造函数和应用程序服务之前,我需要做对这一点。

如果必须使用指定的 StateIdCountryId 创建您的 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 对象。)