在一个非常简单的应用程序中
在某些应用程序中,领域模型和数据库实体是相同的,不需要在它们之间做任何数据映射。我们称它们为“域实体”。在此类应用程序中,DbContext
可以同时充当存储库和工作单元。我们可以简单地使用上下文,而不是做一些复杂的模式:
public class CustomerController : Controller
{
private readonly CustomerContext context; // injected
[HttpPost]
public IActionResult Update(CustomerUpdateDetails viewmodel)
{
// [Repository] acting like an in-memory domain object collection
var person = context.Person.Find(viewmodel.Id);
// [UnitOfWork] keeps track of everything you do during a business transaction
person.Name = viewmodel.NewName;
person.AnotherComplexOperationWithBusinessRequirements();
// [UnitOfWork] figures out everything that needs to be done to alter the database
context.SaveChanges();
}
}
大型应用程序上的复杂查询
如果您的应用程序变得更复杂,您将开始编写一些大型 Linq 查询以访问您的数据。在这种情况下,您可能需要引入一个新层来处理这些查询,以防止您自己将它们复制粘贴到您的控制器中。在这种情况下,您最终将拥有两个不同的层,一个是由 实现的工作单元模式,另一个DbContext
是存储库模式,它将简单地提供一些在前者之上执行的 Linq 结果。您的控制器应该调用存储库来获取实体,更改它们的状态,然后调用 DbContext 将更改持久保存到数据库,但是DbContext.SaveChanges()
通过存储库对象代理是一个可接受的近似值:
public class PersonRepository
{
private readonly PersonDbContext context;
public Person GetClosestTo(GeoCoordinate location) {} // redacted
}
public class PersonController
{
private readonly PersonRepository repository;
private readonly PersonDbContext context; // requires to Equals repository.context
public IActionResult Action()
{
var person = repository.GetClosestTo(new GeoCoordinate());
person.DoSomething();
context.SaveChanges();
// repository.SaveChanges(); would save the injection of the DbContext
}
}
DDD 应用程序
当域模型和实体是两组不同的类时,它会变得更有趣。这将在您开始实施 DDD 时发生,因为这需要您定义一些聚合,它们是可以被视为单个单元的域对象集群。聚合的结构并不总是完美地映射到您的关系数据库模式,因为它可以根据您正在处理的用例提供多层次的抽象。
例如,聚合可能允许用户管理多个地址,但在另一个业务环境中,您可能希望展平模型并将人员地址的建模限制为仅最新值:
public class PersonEntity
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public bool IsValid { get; set; }
public ICollection<AddressEntity> Addresses { get; set; }
}
public class AddressEntity
{
[Key]
public int Id { get; set; }
public string Value { get; set; }
public DateTime Since { get; set; }
public PersonEntity Person { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string CurrentAddressValue { get; private set; }
}
实现工作单元模式
首先让我们回到定义:
一个工作单元跟踪您在业务事务期间所做的所有可能影响数据库的事情。完成后,它会计算出根据您的工作更改数据库需要做的所有事情。
DbContext
跟踪发生在实体上的每一次修改,并在您调用该方法后将它们持久保存到数据库中SaveChanges()
。就像在更简单的示例中一样,工作单元正是DbContext
它所做的,并且将其用作工作单元实际上是 Microsoft 建议您使用 DDD 构建 .NET 应用程序的方式。
实现存储库模式
再一次,让我们回到定义:
存储库在域和数据映射层之间进行调解,就像内存中的域对象集合一样。
,DbContext
不能用作存储库。尽管它表现为实体的内存集合,但它不充当域对象的内存集合。在这种情况下,我们必须为存储库实现另一个类,它将充当我们在内存中的域模型集合,并将数据从实体映射到域模型。但是,您会发现很多实现只是 DbSet 在域模型中的投影,并提供了类似IList
方法,这些方法简单地将实体映射回来并在DbSet<T>
.
尽管此实现可能在多种情况下都有效,但它过分强调了集合部分,而对定义的中介部分则不够重视。
存储库是域层和基础设施层之间的中介,这意味着它的接口是在域层中定义的。接口中描述的方法是在领域层定义的,它们都必须在程序的业务上下文中有意义。无处不在的语言是 DDD 的核心概念,这些方法必须提供一个有意义的名称,而“添加人员”可能不是为该操作命名的正确业务方式。
此外,所有与持久性相关的概念都严格限于存储库的实现。该实现定义了给定的业务操作如何在基础设施层中转换为一系列实体操作,这些实体操作最终将通过原子数据库事务持久化到数据库中。另请注意,Add
域模型上的操作不一定意味着INSERT
数据库中的语句,Remove
有时会以一个UPDATE
甚至多个INSERT
语句结束!
实际上,这是一个非常有效的存储库模式实现:
public class Person
{
public void EnsureEnrollable(IPersonRepository repository)
{
if(!repository.IsEnrollable(this))
{
throw new BusinessException<PersonError>(PersonError.CannotEnroll);
}
}
}
public class PersonRepository : IPersonRepository
{
private readonly PersonDbContext context;
public IEnumerable<Person> GetAll()
{
return context.Persons.AsNoTracking()
.Where(person => person.Active)
.ProjectTo<Person>().ToList();
}
public Person Enroll(Person person)
{
person.EnsureEnrollable(this);
context.Persons.Find(person.Id).Active = true;
context.SaveChanges(); // UPDATE statement
return person;
}
public bool IsEnrollable(Person person)
{
return context.Persons.Any(entity => entity.Id == person.Id && !entity.Active);
}
}
商业交易
您说使用工作单元的目的是形成业务事务,这是错误的。工作单元类的目的是跟踪您在业务事务期间所做的所有可能影响数据库的操作,并根据您在原子操作中的工作来更改数据库。存储库确实共享工作单元实例,但请记住,依赖注入通常在注入 dbcontext 时使用作用域生命周期管理器。这意味着实例只在同一个 http 请求上下文中共享,不同的请求不会共享更改跟踪。使用单例生命周期管理器将在不同的 http 请求之间共享实例,这将在您的应用程序中引发严重破坏。
从存储库调用工作单元保存更改方法实际上是您实现 DDD 应用程序的方式。存储库是了解持久层实际实现的类,它将协调所有数据库操作以在事务结束时提交/回滚。调用 save changes 时从另一个存储库保存更改也是工作单元模式的预期行为。工作单元累积所有存储库所做的所有更改,直到有人调用提交或回滚。如果存储库对不希望保留在数据库中的上下文进行更改,那么问题不在于持久化这些更改的工作单元,而是存储库执行这些更改。
但是,如果您的应用程序执行一个原子保存更改,该更改会保留来自多个存储库的更改操作,则它可能违反了 DDD 设计原则之一。存储库是与聚合的一对一映射,聚合是可以被视为单个单元的域对象集群。如果您正在使用多个存储库,那么您正在尝试在单个事务中修改多个数据单元。
要么您的聚合设计太小,您需要创建一个更大的聚合来保存单个事务的所有数据,并使用一个存储库来处理单个事务中的所有数据;要么您尝试进行跨越模型大部分的复杂事务,并且您需要以最终一致性来实现此事务。