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 Fowler

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

2. UOW的本質

通過以上的介紹,我們可以總結出實現UOW的幾個要點:

  1. UOW跟蹤變化
  2. UOW維護了一個變更列表
  3. UOW將跟蹤到的已變更的對象保存到變更列表中
  4. UOW借助事務一次性提交變更列表中的所有更改
  5. 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基於DDD分層思想實現的源碼: GitHub--UnitOfWork


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM