原文链接:https://www.cnblogs.com/sheng-jie/p/7416302.html

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(, changes); var savedCustomer = InMemorySqliteTestDbContext.Customers
.FirstOrDefault(c => c.NickName == "圣杰"); Assert.Equal("shengjie", savedCustomer.ApplicationUser.UserName); Assert.Equal(customer.ApplicationUserId, savedCustomer.ApplicationUserId); Assert.Equal(, 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. 实现泛型仓储

namespace 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

作者:『圣杰』
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。
 
 
 

UnitOfWork知多少 【转】的更多相关文章

  1. UnitOfWork知多少

    1. 引言 Maintains a list of objects affected by a business transaction and coordinates the writing out ...

  2. Java实现DDD中UnitOfWork

    Java实现DDD中UnitOfWork 背景 Maintains a list of objects affected by a business transaction and coordinat ...

  3. uow Unit of work

    通过学习圣杰的文章  UnitOfWork知多少  知道uow其实就是为了解决 一次提交所有更改 1.ef本身可以具备这样一个功能,但是我们在写仓储的实现的时候 经常会直接显式saveChanges了 ...

  4. UnitOfWork机制的实现和注意事项

    UnitOfWork机制 /*一点牢骚: * UnitOfWork机制的蛋疼之处: *    UnitOfWork机制,决定了插入新的实体前,要预先设置数据库中的主键Id,尽管数据库自己生产主键. * ...

  5. eShopOnContainers 知多少[8]:Ordering microservice

    1. 引言 Ordering microservice(订单微服务)就是处理订单的了,它与前面讲到的几个微服务相比要复杂的多.主要涉及以下业务逻辑: 订单的创建.取消.支付.发货 库存的扣减 2. 架 ...

  6. 谈谈一些有趣的CSS题目(十一)-- reset.css 知多少?

    开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...

  7. Python 爬虫模拟登陆知乎

    在之前写过一篇使用python爬虫爬取电影天堂资源的博客,重点是如何解析页面和提高爬虫的效率.由于电影天堂上的资源获取权限是所有人都一样的,所以不需要进行登录验证操作,写完那篇文章后又花了些时间研究了 ...

  8. 谈谈一些有趣的CSS题目(三)-- 层叠顺序与堆栈上下文知多少

    开本系列,讨论一些有趣的 CSS 题目,抛开实用性而言,一些题目为了拓宽一下解决问题的思路,此外,涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题中有你感觉 ...

  9. scrapy 知乎用户信息爬虫

    zhihu_spider 此项目的功能是爬取知乎用户信息以及人际拓扑关系,爬虫框架使用scrapy,数据存储使用mongo,下载这些数据感觉也没什么用,就当为大家学习scrapy提供一个例子吧.代码地 ...

随机推荐

  1. 【LOJ6043】「雅礼集训 2017 Day7」蛐蛐国的修墙方案(搜索技巧题)

    点此看题面 大致题意: 给你一个长度为\(n\)的排列\(p\),要求构造一个合法的括号序列,使得如果第\(i\)个位置是左括号,则第\(p_i\)个位置一定是右括号. 暴搜 很容易想出一个暴搜. 即 ...

  2. 字符串处理,Poj(2121)

    题目链接:http://poj.org/problem?id=2121 差一点就WA哭了,主要是自己傻逼了. 思路: 遇到hundred,sum*100; 但是遇到thouthend,million, ...

  3. 20145238-荆玉茗 《Java程序设计》第7周学习总结

    20145238 <Java程序设计>第7周学习总结 教材学习内容总结 第13章时间与日期 13.1.1 ·即使标注为GMT(格林威治时间),实际上谈到的的是UTC(Unix时间)时间. ...

  4. windows 平台使用 VS2017 编译 libevent 源码

    一 依赖库编译 先要将其依赖的库编译好,其中openssl需要编译到libevent中,编译成libevent_openssl.lib库,zlib在新版本中只有示例用到. 1)windows 平台使用 ...

  5. 前端面试整理(HTML&CSS)

    1.行内元素和块级元素?img算什么?行内元素怎么转化为块级元素? 行内元素:和有他元素都在一行上,高度.行高及外边距和内边距都不可改变,文字图片的宽度不可改变,只能容纳文本或者其他行内元素:其中im ...

  6. EBS R12中FND凭证打印警告:OPP响应超时

    接近年关,最近年结忙的飞起,此为背景,今天运维那边反应日记账凭证打印报错,看了下后台请求发现请求有警告. 查看日志发现报了“并发:OPP响应超时”的警告,这个地方响应超时可能是配置文件中“并发:OPP ...

  7. 关于ACL中通配符掩码(反掩码)认识

    ACL(Access Control List)  访问控制列表在作为数据包的过滤器以及在对指定的某种类型的数据包的优先级,起到了对某些数据包的优先级起到了限制流量的作用,减少了网络的拥塞.      ...

  8. 在ubuntu中docker的简单使用(一)

    >>docker version 当运行docker version 命令出现Cannot connect to Docker daemon. Is the docker daemon r ...

  9. 截取前后缀FOR C

    memcpy(new, old + prefix_len, sizeof(new)); memcpy(new, old, strlen(old) - suffix_len); :)

  10. python生成xml文件

    先上代码: #!/usr/bin/env python3 # _*_ coding: utf-8 _*_ from xml.dom.minidom import Document def readFi ...