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實現的源碼: GitHub--UnitOfWork

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • DDD理論學習系列——案例及目錄 1. 引言 DDD中的Repository,主要有兩種翻譯:資源庫和倉儲,本文取...
    圣杰閱讀 6,566評論 9 14
  • 本文將介紹聚合以及與其高度相關的并發主題。我在之前已經說過,初學者第一步需要將業務邏輯盡量放到實體或值對象中,給實...
    Bobby0322閱讀 1,095評論 0 4
  • 山高高,云渺渺,縱身一躍往下跳。 山崖共身葬千古,云翳朵朵浪滔滔。 美景使人怡心曠,拋卻名利是非怨。 曾因金銀迷雙...
    d03e056874dc閱讀 344評論 0 0
  • 一個五一,一場聚會,十多年前的同事,大部分已經離開公司多年,甚至有的已經離開深圳,但還是偶爾可以聚在一起,吃吃飯,...
    云沐媽媽閱讀 227評論 0 0