系列目录 1.net core天马行空系列:原生DI+AOP实现spring boot注解式编程 哈哈哈哈,大家好,我就是那个高产似母猪的三合,长久以来,我一直在思考,如何才能实现高效而简洁的仓储模式(不是DDD里的仓储,更准确的说就是数据库表的mapper),实现spring boot里那样利用注解实现事物操作,日有所思,终有所得,本篇文章浓缩了我对于仓储模式和工作单元模式理解的精华,希望能对大家有所帮助,如果哪里说错了,也希望大家不吝赐教。由于ef core本身就实现了这2种模式,再在ef core的基础上进行封装就失去了学习的意义,所以本文用到的是ORM方案是dapper+dapper.contrib, 这2个库皆出自名门stackexchange,也就是大名鼎鼎的爆栈啦,他们出品的库还有StackExchange.Redis,所以品质自不用说,开始正文前,先在nuget上安装这2个库。BTW,动态代理,注解式编程,AOP贯穿本系列始终,no bb,正文开始。 1.定义用到的类 上次讲飙车,这次我们讲,去加油站加油,加油这个过程呢,存在一个事物操作,那就是,加油站必须给我加足够的油,我才给他付钱,有点像银行转账,那么引申出2张表,汽车油量表(oilQuantity)和现金余额表(cashBalance),对应的表结构和实体类如下,都比较简单,除了主键id,oilQuantity表只有一个油量quantity字段,cashBalance表只有一个余额balance字段,数据库使用的是mysql,实体类的注解TableAttribute使用的命名空间是System.ComponentModel.DataAnnotations.Schema。 复制代码 CREATE TABLE test.oilQuantity ( id INT NOT NULL AUTO_INCREMENT, quantity DECIMAL NULL, CONSTRAINT caroil_pk PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; 复制代码 复制代码 CREATE TABLE test.cashBalance ( id INT NOT NULL AUTO_INCREMENT, balance DECIMAL NOT NULL, CONSTRAINT cashbalance_pk PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; 复制代码 复制代码 [Table("OilQuantity")] public class OilQuantity { [Key] public int Id { set; get; } /// /// 油量 /// public decimal Quantity { set; get; } } 复制代码 复制代码 [Table("CashBalance")] public class CashBalance { [Key] public int Id { set; get; } /// /// 余额 /// public decimal Balance { set; get; } } 复制代码 定义数据库链接工厂类接口IDbFactory和他的实现类DbFactory,这个类主要负责数据库链接的创建,链接分为2种,一种是短链接,不开启事物的时候使用,用完即毁,每次获得都是全新的链接,另一种是长链接,用在事物操作中,DbFactory本身注册为scope级别,长链接创建后会保存在DbFactory的属性中,所以变相的实现了scope级别,同理,长链接的事务开启后也会被保存,用来在仓储中实现事物操作。 复制代码 public interface IDbFactory:IDisposable { /// /// 长链接 /// IDbConnection LongDbConnection { get; } /// /// 长链接的事物 /// IDbTransaction LongDbTransaction { get; } /// /// 短链接 /// IDbConnection ShortDbConnection { get; } /// /// 开启事务 /// void BeginTransaction(); } 复制代码 复制代码 /// /// 负责生成和销毁数据库链接 /// public class DbFactory:IDbFactory { [Value("MysqlConnectionStr")] public string MysqlConnectionStr { set; get; } /// /// 长连接 /// public IDbConnection LongDbConnection { private set; get; } /// /// 长连接的事物 /// public IDbTransaction LongDbTransaction { private set; get; } /// /// 短链接 /// public IDbConnection ShortDbConnection { get { var dbConnection = new MySqlConnection(MysqlConnectionStr); dbConnection.Open(); return dbConnection; } } /// /// 开启事务 /// /// public void BeginTransaction() { if (LongDbConnection == null) { LongDbConnection = new MySqlConnection(MysqlConnectionStr); LongDbConnection.Open(); LongDbTransaction = LongDbConnection.BeginTransaction(); } } public void Dispose() { LongDbTransaction?.Dispose(); if (LongDbConnection?.State != ConnectionState.Closed) { LongDbConnection?.Close(); } LongDbConnection?.Dispose(); LongDbTransaction = null; LongDbConnection = null; } } 复制代码 定义工作单元接口IUnitOfWork和他的实现类UnitOfWork,可以看到,IUnitOfWork中有一个引用次数ActiveNumber的属性,这个属性的作用主要是,如果一个标注了[Transactional]的方法里嵌套了另一个标注了[Transactional]的方法,我们就可以通过计数来确认,具体谁才是最外层的方法,从而达到不在内层方法里开启另一个事物,并且在内层方法结束时不会提前提交事务的效果。同时呢,UnitOfWork只负责与事务有关的操作,其他创建链接,创建事物等操作,都是由注入的IDbFactory完成的。 复制代码 public interface IUnitOfWork : IDisposable { /// /// 引用次数,开启一次事物加+1,当次数为0时提交,主要是为了防止事物嵌套 /// int ActiveNumber { get; set; } /// /// 开启事务 /// void BeginTransaction(); /// /// 提交 /// void Commit(); /// /// 事物回滚 /// void RollBack(); } 复制代码 复制代码 public class UnitOfWork : IUnitOfWork { /// /// 工作单元引用次数,当次数为0时提交,主要为了防止事物嵌套 /// public int ActiveNumber { get; set; } = 0; [Autowired] public IDbFactory DbFactory { set; get; } public void BeginTransaction() { if (this.ActiveNumber == 0) { DbFactory.BeginTransaction(); Console.WriteLine("开启事务"); } this.ActiveNumber++; } public void Commit() { this.ActiveNumber--; if (this.ActiveNumber == 0) { if (DbFactory.LongDbConnection != null) { try { DbFactory.LongDbTransaction.Commit(); } catch (Exception e) { DbFactory.LongDbTransaction.Rollback(); Console.WriteLine(e); throw; } finally { this.Dispose(); } } Console.WriteLine("提交事务"); } } public void Dispose() { DbFactory.Dispose(); } public void RollBack() { if (this.ActiveNumber > 0 && DbFactory.LongDbTransaction != null) { try { DbFactory.LongDbTransaction.Rollback(); } catch (Exception e) { Console.WriteLine(e); throw; } } Console.WriteLine("回滚事务"); } } 复制代码 泛型仓储接口IRepository和他的实现类BaseRepository,为了偷懒,只写了同步部分,异步同理,若使用异步,拦截器也要使用异步拦截器。BaseRepository中通过属性注入了IUnitOfWork和IDbFactory,IUnitOfWork主要负责告诉仓储,该使用长连接还是短链接,IDbFactory负责提供具体的链接和事物,而更细节的crud操作,则都是由dapper和dapper.contrib完成的。看代码var dbcon = Uow.ActiveNumber == 0 ? DbFactory.ShortDbConnection : DbFactory.LongDbConnection;可以看到通过判断uow的引用计数ActiveNumber 来判断使用的是长链接还是短链接,并且如果ActiveNumber==0的话,在数据库操作结束后就会释放掉链接。 复制代码 public interface IRepository where T : class { IList GetAll(); T Get(object id); T Insert(T t); IList Insert(IList t); void Update(T t); void Update(IList t); void Delete(IList t); void Delete(T t); } 复制代码 View Code 事物拦截器TransactionalInterceptor,在方法开始前,如果拦截到的方法具有[TransactionalAttribute]注解,则通过uow开启事务,在方法结束后,如果拦截到的方法具有[TransactionalAttribute]注解,则通过uow结束事务。 复制代码 /// /// 事物拦截器 /// public class TransactionalInterceptor : StandardInterceptor { private IUnitOfWork Uow { set; get; } public TransactionalInterceptor(IUnitOfWork uow) { Uow = uow; } protected override void PreProceed(IInvocation invocation) { Console.WriteLine("{0}拦截前", invocation.Method.Name); var method = invocation.MethodInvocationTarget; if (method != null && method.GetCustomAttribute() != null) { Uow.BeginTransaction(); } } protected override void PerformProceed(IInvocation invocation) { invocation.Proceed(); } protected override void PostProceed(IInvocation invocation) { Console.WriteLine("{0}拦截后, 返回值是{1}", invocation.Method.Name, invocation.ReturnValue); var method = invocation.MethodInvocationTarget; if (method != null && method.GetCustomAttribute() != null) { Uow.Commit(); } } } 复制代码 IServiceCollection静态扩展类SummerBootExtentions.cs,和上一篇比较,主要就是添加了AddSbRepositoryService方法,这个方法主要通过反射获得由[TableAttribute]标注的实体类,并向IServiceCollection中添加相应的的仓储接口和相应的仓储实现类,为什么不用services.AddScoped(typeof(IRepository<>),typeof(BaseRepository<>));这种方法注入泛型仓储呢?因为net core原生DI并不支持泛型注入的工厂委托创建,那么就无法实现动态代理了,所以采用变通的方法,将通用泛型接口,转成具体的泛型接口,SummerBootExtentions.cs的另一个变动就是将ProxyGenerator注册成单例了,这样就可以利用缓存,提高创建动态代理的性能,SummerBootExtentions.cs代码如下: View Code 定义一个加油的服务类接口IAddOilService和接口的实现类AddOilService,可以从代码中看到,我们通过属性注入添加了CashBalanceRepository和OilQuantityRepository,通过[Transactional]标注AddOil方法,使其成为事物性操作,AddOil主要就是初始化金额和油量,然后进行加减操作,最后更新。 复制代码 public interface IAddOilService { void AddOil(); } 复制代码 复制代码 public class AddOilService : IAddOilService { [Autowired] public IRepository CashBalanceRepository { set; get; } [Autowired] public IRepository OilQuantityRepository { set; get; } [Transactional] public void AddOil() { //初始化金额 var cashBalance = CashBalanceRepository.Insert(new CashBalance() { Balance = 100 }); //初始化油量 var oilQuantity = OilQuantityRepository.Insert(new OilQuantity() { Quantity = 5 }); cashBalance.Balance -= 95; oilQuantity.Quantity += 50; CashBalanceRepository.Update(cashBalance); //throw new Exception("主动报错"); OilQuantityRepository.Update(oilQuantity); } } 复制代码 修改Startup.cs中的ConfigureServices方法,代码如下: 复制代码 public void ConfigureServices(IServiceCollection services) { services.Configure(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1) .AddSB(); services.AddSingleton(); services.AddSbScoped(typeof(TransactionalInterceptor)); services.AddSbScoped(); services.AddScoped(typeof(TransactionalInterceptor)); services.AddSbScoped(typeof(TransactionalInterceptor)); services.AddSbScoped(); services.AddSbRepositoryService(typeof(TransactionalInterceptor)); services.AddSbScoped(typeof(TransactionalInterceptor)); } 复制代码 控制器HomeController 复制代码 public class HomeController : Controller { [Autowired] public ICar Car { set; get; } [Autowired] public IDistributedCache Cache { set; get; } [Value("description")] public string Description { set; get; } [Autowired] public IAddOilService AddOilService { set; get; } public IActionResult Index() { var car = Car; AddOilService.AddOil(); Car.Fire(); Console.WriteLine(Description); return View(); } } 复制代码 2.效果图 2.1 清空2张表里的数据,在AddOil末尾打断点。 虽然前面执行了insert操作,但是我们查询2张表,发现里面并没有新增数据,因为事物还未提交,符合预期。从断点处继续执行,然后查询数据库。 执行完事物后,数据正确,符合预期。 2.2 清空2张表里的数据,注释掉AddOil方法的[Transactional]注解,在AddOil末尾打断点。 查看数据库,因为没开启事务,所以数据已经正确插入到表中,并且由于oilQuantity仓储未更新,所以数值正确,从断点处继续执行 oilQuantity仓储更新,数值正确,符合预期。 2.3 清空2张表里的数据,开启AddOil方法的[Transactional]注解,并在方法中主动抛出一个错误。 表中并未添加数据,因为事物未提交,回滚了,符合预期。 BTW,事物的开启,除了使用[Transactional]注解外,也可以通过注入uow,手动开启和提交。 3. 写在最后 只需要在数据库实体类上注解[Table("表名")]就可以直接使用仓储了,是不是很简洁优雅呢?这里实现的仓储都是通用的,如果有特殊需求的仓储,则需要自定义接口和实现类,接口继承IRepository,实现类继承BaseRepository,然后注入自己的特殊仓储就行了。https://www.cnblogs.com/hezp/p/11434046.html