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阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • DDD理论学习系列——案例及目录 1. 引言 DDD中的Repository,主要有两种翻译:资源库和仓储,本文取...
    圣杰阅读 6,513评论 9 14
  • 本文将介绍聚合以及与其高度相关的并发主题。我在之前已经说过,初学者第一步需要将业务逻辑尽量放到实体或值对象中,给实...
    Bobby0322阅读 1,075评论 0 4
  • 山高高,云渺渺,纵身一跃往下跳。 山崖共身葬千古,云翳朵朵浪滔滔。 美景使人怡心旷,抛却名利是非怨。 曾因金银迷双...
    d03e056874dc阅读 327评论 0 0
  • 一个五一,一场聚会,十多年前的同事,大部分已经离开公司多年,甚至有的已经离开深圳,但还是偶尔可以聚在一起,吃吃饭,...
    云沐妈妈阅读 223评论 0 0