UnitOfWork知多少

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 Fowlerhtml

Unit Of Work模式,由馬丁大叔提出,是一種數據訪問模式。UOW模式的做用是在業務用例的操做中跟蹤對象的全部更改(增長、刪除和更新),並將全部更改的對象保存在其維護的列表中。在業務用例的終點,經過事務,一次性提交全部更改,以確保數據的完整性和有效性。總而言之,UOW協調這些對象的持久化及併發問題。git

2. UOW的本質

經過以上的介紹,咱們能夠總結出實現UOW的幾個要點:github

  1. UOW跟蹤變化
  2. UOW維護了一個變動列表
  3. UOW將跟蹤到的已變動的對象保存到變動列表中
  4. UOW藉助事務一次性提交變動列表中的全部更改
  5. UOW處理併發

而對於這些要點,EF中的DBContext已經實現了。數據庫

3. EF中的UOW

每一個DbContext類型實例都有一個ChangeTracker用來跟蹤記錄實體的變化。當調用SaveChanges時,全部的更改將經過事務一次性提交到數據庫。併發

咱們直接看個EF Core的測試用例:ide

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負責管理倉儲處理事務,倉儲管理單一聚合,聚合又由實體和值對象組成。ui

下面咱們就先來定義實體和值對象,這裏咱們使用層超類型。this

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基於DDD分層思想實現的源碼: GitHub--UnitOfWork

相關文章
相關標籤/搜索