1. 引言
Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.
Unit of Work --Martin Fowler
Unit Of Work模式,由馬丁大叔提出,是一種數據訪問模式。UOW模式的作用是在業務用例的操作中跟蹤對象的所有更改(增加、刪除和更新),并將所有更改的對象保存在其維護的列表中。在業務用例的終點,通過事務,一次性提交所有更改,以確保數據的完整性和有效性。總而言之,UOW協調這些對象的持久化及并發問題。
2. UOW的本質
通過以上的介紹,我們可以總結出實現UOW的幾個要點:
- UOW跟蹤變化
- UOW維護了一個變更列表
- UOW將跟蹤到的已變更的對象保存到變更列表中
- UOW借助事務一次性提交變更列表中的所有更改
- UOW處理并發
而對于這些要點,EF中的DBContext已經實現了。
3. EF中的UOW
每個DbContext
類型實例都有一個ChangeTracker
用來跟蹤記錄實體的變化。當調用SaveChanges
時,所有的更改將通過事務一次性提交到數據庫。
我們直接看個EF Core的測試用例:
public ApplicationDbContext InMemorySqliteTestDbContext
{
get
{
// In-memory database only exists while the connection is open
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.Options;
var context = new ApplicationDbContext(options);
context.Database.EnsureCreated();
return context;
}
}
[Fact]
public void Test_Ef_Implemented_Uow()
{
//新增用戶
var user = new ApplicationUser()
{
UserName = "shengjie",
Email = "ysjshengjie@qq.com"
};
InMemorySqliteTestDbContext.Users.Add(user);
//創建用戶對應客戶
var customer = new Customer()
{
ApplicationUser = user,
NickName = "圣杰"
};
InMemorySqliteTestDbContext.Customers.Add(customer);
//添加地址
var address = new Address("廣東省", "深圳市", "福田區", "下沙街道", "圣杰", "135****9309");
InMemorySqliteTestDbContext.Addresses.Add(address);
//修改客戶對象的派送地址
customer.AddShippingAddress(address);
InMemoryTestDbContext.Entry(customer).State = EntityState.Modified;
//保存
var changes = InMemorySqliteTestDbContext.SaveChanges();
Assert.Equal(3, changes);
var savedCustomer = InMemorySqliteTestDbContext.Customers
.FirstOrDefault(c => c.NickName == "圣杰");
Assert.Equal("shengjie", savedCustomer.ApplicationUser.UserName);
Assert.Equal(customer.ApplicationUserId, savedCustomer.ApplicationUserId);
Assert.Equal(1, savedCustomer.ShippingAddresses.Count);
}
首先這個用例是綠色通過的。該測試用例中我們添加了一個User,并為User創建對應的Customer,同時為Customer添加一條Address。從代碼中我們可以看出僅做了一次保存,新增加的User、Customer、Address對象都成功持久化到了內存數據庫中。從而證明EF Core是實現了Uow模式的。但很顯然應用程序與基礎設施層高度耦合,那如何解耦呢?繼續往下看。
4. DDD中的UOW
那既然EF Core已經實現了Uow模式,我們還有必要自行實現一套Uow模式嗎?這就視具體情況而定了,如果你的項目簡單的增刪改查就搞定了的,就不用折騰了。
在DDD中,我們會借助倉儲模式來實現領域對象的持久化。倉儲只關注于單一聚合的持久化,而業務用例卻常常會涉及多個聚合的更改,為了確保業務用例的一致型,我們需要引入事務管理,而事務管理是應用服務層的關注點。我們如何在應用服務層來管理事務呢?借助UOW。這樣就形成了一條鏈:Uow->倉儲-->聚合-->實體和值對象。即Uow負責管理倉儲處理事務,倉儲管理單一聚合,聚合又由實體和值對象組成。
下面我們就先來定義實體和值對象,這里我們使用層超類型。
4.1. 定義實體
/// <summary>
/// A shortcut of <see cref="IEntity{TPrimaryKey}"/> for most used primary key type (<see cref="int"/>).
/// </summary>
public interface IEntity : IEntity<int>
{
}
/// <summary>
/// Defines interface for base entity type. All entities in the system must implement this interface.
/// </summary>
/// <typeparam name="TPrimaryKey">Type of the primary key of the entity</typeparam>
public interface IEntity<TPrimaryKey>
{
/// <summary>
/// Unique identifier for this entity.
/// </summary>
TPrimaryKey Id { get; set; }
}
4.2. 定義聚合
namespace UnitOfWork
{
public interface IAggregateRoot : IAggregateRoot<int>, IEntity
{
}
public interface IAggregateRoot<TPrimaryKey> : IEntity<TPrimaryKey>
{
}
}
4.3. 定義泛型倉儲
namespace UnitOfWork
{
public interface IRepository<TEntity> : IRepository<TEntity, int>
where TEntity : class, IEntity, IAggregateRoot
{
}
public interface IRepository<TEntity, TPrimaryKey>
where TEntity : class, IEntity<TPrimaryKey>, IAggregateRoot<TPrimaryKey>
{
IQueryable<TEntity> GetAll();
TEntity Get(TPrimaryKey id);
TEntity FirstOrDefault(TPrimaryKey id);
TEntity Insert(TEntity entity);
TEntity Update(TEntity entity);
void Delete(TEntity entity);
void Delete(TPrimaryKey id);
}
}
因為倉儲是管理聚合的,所以我們需要限制泛型參數為實現IAggregateRoot
的類。
4.4. 實現泛型倉儲
amespace UnitOfWork.Repositories
{
public class EfCoreRepository<TEntity>
: EfCoreRepository<TEntity, int>, IRepository<TEntity>
where TEntity : class, IEntity, IAggregateRoot
{
public EfCoreRepository(UnitOfWorkDbContext dbDbContext) : base(dbDbContext)
{
}
}
public class EfCoreRepository<TEntity, TPrimaryKey>
: IRepository<TEntity, TPrimaryKey>
where TEntity : class, IEntity<TPrimaryKey>, IAggregateRoot<TPrimaryKey>
{
private readonly UnitOfWorkDbContext _dbContext;
public virtual DbSet<TEntity> Table => _dbContext.Set<TEntity>();
public EfCoreRepository(UnitOfWorkDbContext dbDbContext)
{
_dbContext = dbDbContext;
}
public IQueryable<TEntity> GetAll()
{
return Table.AsQueryable();
}
public TEntity Insert(TEntity entity)
{
var newEntity = Table.Add(entity).Entity;
_dbContext.SaveChanges();
return newEntity;
}
public TEntity Update(TEntity entity)
{
AttachIfNot(entity);
_dbContext.Entry(entity).State = EntityState.Modified;
_dbContext.SaveChanges();
return entity;
}
public void Delete(TEntity entity)
{
AttachIfNot(entity);
Table.Remove(entity);
_dbContext.SaveChanges();
}
public void Delete(TPrimaryKey id)
{
var entity = GetFromChangeTrackerOrNull(id);
if (entity != null)
{
Delete(entity);
return;
}
entity = FirstOrDefault(id);
if (entity != null)
{
Delete(entity);
return;
}
}
protected virtual void AttachIfNot(TEntity entity)
{
var entry = _dbContext.ChangeTracker.Entries().FirstOrDefault(ent => ent.Entity == entity);
if (entry != null)
{
return;
}
Table.Attach(entity);
}
private TEntity GetFromChangeTrackerOrNull(TPrimaryKey id)
{
var entry = _dbContext.ChangeTracker.Entries()
.FirstOrDefault(
ent =>
ent.Entity is TEntity &&
EqualityComparer<TPrimaryKey>.Default.Equals(id, ((TEntity)ent.Entity).Id)
);
return entry?.Entity as TEntity;
}
}
}
因為我們直接使用EF Core進行持久化,所以我們直接通過構造函數初始化DbContex實例。同時,我們注意到Insert、Update、Delete
方法都顯式的調用了SaveChanges
方法。
至此,我們完成了從實體到聚合再到倉儲的定義和實現,萬事俱備,只欠Uow。
4.5. 實現UOW
通過第3節的說明我們已經知道,EF Core已經實現了UOW模式。而為了確保領域層透明的進行持久化,我們對其進行了更高一層的抽象,實現了倉儲模式。但這似乎引入了另外一個問題,因為倉儲是管理單一聚合的,每次做增刪改時都顯式的提交了更改(調用了SaveChanges),在處理多個聚合時,就無法利用DbContext進行批量提交了。那該如何是好?一不做二不休,我們再對其進行一層抽象,抽離保存接口,這也就是Uow的核心接口方法。
我們抽離SaveChanges
方法,定義IUnitOfWork
接口。
namespace UnitOfWork
{
public interface IUnitOfWork
{
int SaveChanges();
}
}
因為我們是基于EFCore實現Uow的,所以我們只需要依賴DbContex,就可以實現批量提交。實現也很簡單:
namespace UnitOfWork
{
public class UnitOfWork<TDbContext> : IUnitOfWork where TDbContext : DbContext
{
private readonly TDbContext _dbContext;
public UnitOfWork(TDbContext context)
{
_dbContext = context ?? throw new ArgumentNullException(nameof(context));
}
public int SaveChanges()
{
return _dbContext.SaveChanges();
}
}
}
既然Uow接手保存操作,自然我們需要:注釋掉EfCoreRepository中Insert、Update、Delete方法中的顯式保存調用_dbContext.SaveChanges();
。
那如何確保操作多個倉儲時,最終能夠一次性提交所有呢?
確保Uow和倉儲共用同一個DbContex即可。這個時候我們就可以借助依賴注入。
4.6. 依賴注入
我們直接使用.net core 提供的依賴注入,依次注入DbContext、UnitOfWork和Repository。
//注入DbContext
services.AddDbContext<UnitOfWorkDbContext>(
options =>options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
//注入Uow依賴
services.AddScoped<IUnitOfWork, UnitOfWork<UnitOfWorkDbContext>>();
//注入泛型倉儲
services.AddTransient(typeof(IRepository<>), typeof(EfCoreRepository<>));
services.AddTransient(typeof(IRepository<,>), typeof(EfCoreRepository<,>));
這里我們限定了DbContext和UnitOfWork的生命周期為Scoped
,從而確保每次請求共用同一個對象。如何理解呢?就是整個調用鏈上的需要注入的同類型對象,使用是同一個類型實例。
4.7. 使用UOW
下面我們就來實際看一看如何使用UOW,我們定義一個應用服務:
namespace UnitOfWork.Customer
{
public class CustomerAppService : ICustomerAppService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IRepository<Customer> _customerRepository;
private readonly IRepository<ShoppingCart.ShoppingCart> _shoppingCartRepository;
public CustomerAppService(IRepository<ShoppingCart> shoppingCartRepository,
IRepository<Customer> customerRepository, IUnitOfWork unitOfWork)
{
_shoppingCartRepository = shoppingCartRepository;
_customerRepository = customerRepository;
_unitOfWork = unitOfWork;
}
public void CreateCustomer(Customer customer)
{
_customerRepository.Insert(customer);//創建客戶
var cart = new ShoppingCart.ShoppingCart() {CustomerId = customer.Id};
_shoppingCartRepository.Insert(cart);//創建購物車
_unitOfWork.SaveChanges();
}
//....
}
}
通過以上案例,我們可以看出,我們只需要通過構造函數依賴注入需要的倉儲和Uow即可完成對多個倉儲的持久化操作。
5. 最后
對于Uow模式,有很多種實現方式,大多過于復雜抽象。EF和EF Core本身已經實現了Uow模式,所以在實現時,我們應避免不必要的抽象來降低系統的復雜度。
最后,重申一下:
Uow模式是用來管理倉儲處理事務的,倉儲用來解耦的(領域層與基礎設施層)。而基于EF實現Uow模式的關鍵:確保Uow和Reopository之間共享同一個DbContext實例。
最后附上基于.Net Core和EF Core實現的源碼: GitHub--UnitOfWork