52

net core天马行空系列: 泛型仓储和声明式事物实现最优雅的crud操作 - 三合视角

 4 years ago
source link: https://www.cnblogs.com/hezp/p/11434046.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

net core天马行空系列: 泛型仓储和声明式事物实现最优雅的crud操作

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; }
        /// <summary>
        /// 油量
        /// </summary>
        public decimal Quantity { set; get; }
    }
 [Table("CashBalance")]
    public class CashBalance
    {
        [Key]
        public int Id { set; get; }
        /// <summary>
        /// 余额
        /// </summary>
        public decimal Balance { set; get; }
    }

定义数据库链接工厂类接口IDbFactory和他的实现类DbFactory,这个类主要负责数据库链接的创建,链接分为2种,一种是短链接,不开启事物的时候使用,用完即毁,每次获得都是全新的链接,另一种是长链接,用在事物操作中,DbFactory本身注册为scope级别,长链接创建后会保存在DbFactory的属性中,所以变相的实现了scope级别,同理,长链接的事务开启后也会被保存,用来在仓储中实现事物操作。

 public interface IDbFactory:IDisposable
    {
        /// <summary>
        /// 长链接
        /// </summary>
        IDbConnection LongDbConnection { get; }

        /// <summary>
        /// 长链接的事物
        /// </summary>
        IDbTransaction LongDbTransaction { get; }

        /// <summary>
        /// 短链接
        /// </summary>
        IDbConnection ShortDbConnection { get; }

        /// <summary>
        /// 开启事务
        /// </summary>
        void BeginTransaction();
    }
  /// <summary>
    /// 负责生成和销毁数据库链接
    /// </summary>
    public class DbFactory:IDbFactory
    {
        [Value("MysqlConnectionStr")]
        public string MysqlConnectionStr { set; get; }

        /// <summary>
        /// 长连接
        /// </summary>
        public IDbConnection LongDbConnection { private set; get; }

        /// <summary>
        /// 长连接的事物
        /// </summary>
        public IDbTransaction LongDbTransaction { private set; get; }

        /// <summary>
        /// 短链接
        /// </summary>
        public IDbConnection ShortDbConnection
        {
            get
            {
                var dbConnection = new MySqlConnection(MysqlConnectionStr);
                dbConnection.Open();
                return dbConnection;
            }
        }

        /// <summary>
        /// 开启事务
        /// </summary>
        /// <returns></returns>
        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
    {
        /// <summary>
        /// 引用次数,开启一次事物加+1,当次数为0时提交,主要是为了防止事物嵌套
        /// </summary>
        int ActiveNumber { get; set; }

        /// <summary>
        /// 开启事务
        /// </summary>
        void BeginTransaction();
        
        /// <summary>
        /// 提交
        /// </summary>
        void Commit();

        /// <summary>
        /// 事物回滚
        /// </summary>
        void RollBack();
    }
 public class UnitOfWork : IUnitOfWork
    {
        /// <summary>
        /// 工作单元引用次数,当次数为0时提交,主要为了防止事物嵌套
        /// </summary>
        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<T>和他的实现类BaseRepository<T>,为了偷懒,只写了同步部分,异步同理,若使用异步,拦截器也要使用异步拦截器。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<T> where T : class
    {
        IList<T> GetAll();
        T Get(object id);

        T Insert(T t);
        IList<T> Insert(IList<T> t);

        void Update(T t);
        void Update(IList<T> t);

        void Delete(IList<T> t);
        void Delete(T t);
    }

ContractedBlock.gifExpandedBlockStart.gif

View Code

事物拦截器TransactionalInterceptor,在方法开始前,如果拦截到的方法具有[TransactionalAttribute]注解,则通过uow开启事务,在方法结束后,如果拦截到的方法具有[TransactionalAttribute]注解,则通过uow结束事务。

/// <summary>
    /// 事物拦截器
    /// </summary>
    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<TransactionalAttribute>() != 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<TransactionalAttribute>() != null)
            {
                Uow.Commit();
            }
        }
    }

IServiceCollection静态扩展类SummerBootExtentions.cs,和上一篇比较,主要就是添加了AddSbRepositoryService方法,这个方法主要通过反射获得由[TableAttribute]标注的实体类,并向IServiceCollection中添加相应的的仓储接口和相应的仓储实现类,为什么不用services.AddScoped(typeof(IRepository<>),typeof(BaseRepository<>));这种方法注入泛型仓储呢?因为net core原生DI并不支持泛型注入的工厂委托创建,那么就无法实现动态代理了,所以采用变通的方法,将通用泛型接口,转成具体的泛型接口,SummerBootExtentions.cs的另一个变动就是将ProxyGenerator注册成单例了,这样就可以利用缓存,提高创建动态代理的性能,SummerBootExtentions.cs代码如下:

ContractedBlock.gifExpandedBlockStart.gif

View Code

定义一个加油的服务类接口IAddOilService和接口的实现类AddOilService,可以从代码中看到,我们通过属性注入添加了CashBalanceRepository和OilQuantityRepository,通过[Transactional]标注AddOil方法,使其成为事物性操作,AddOil主要就是初始化金额和油量,然后进行加减操作,最后更新。

   public interface IAddOilService
    {
        void AddOil();
    }
public class AddOilService : IAddOilService
    {
        [Autowired]
        public IRepository<CashBalance> CashBalanceRepository { set; get; }

        [Autowired]
        public IRepository<OilQuantity> 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<CookiePolicyOptions>(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<ProxyGenerator>();

            services.AddSbScoped<Engine>(typeof(TransactionalInterceptor));

            services.AddSbScoped<IUnitOfWork, UnitOfWork>();
            services.AddScoped(typeof(TransactionalInterceptor));

            services.AddSbScoped<ICar, Car>(typeof(TransactionalInterceptor));

            services.AddSbScoped<IDbFactory, DbFactory>();
            services.AddSbRepositoryService(typeof(TransactionalInterceptor));
            services.AddSbScoped<IAddOilService, AddOilService>(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末尾打断点。

1323385-20190904091436868-241645300.jpg

1323385-20190904091500533-189563059.jpg1323385-20190904091516108-1667895100.jpg

虽然前面执行了insert操作,但是我们查询2张表,发现里面并没有新增数据,因为事物还未提交,符合预期。从断点处继续执行,然后查询数据库。

 1323385-20190904091849093-1862204119.jpg1323385-20190904091855864-1509182552.jpg

执行完事物后,数据正确,符合预期。

2.2 清空2张表里的数据,注释掉AddOil方法的[Transactional]注解,在AddOil末尾打断点。

1323385-20190904092502055-25988051.jpg

1323385-20190904092603956-1636121392.jpg1323385-20190904092614448-994336794.jpg

查看数据库,因为没开启事务,所以数据已经正确插入到表中,并且由于oilQuantity仓储未更新,所以数值正确,从断点处继续执行

 1323385-20190904093209910-1556988914.jpg

oilQuantity仓储更新,数值正确,符合预期。

2.3 清空2张表里的数据,开启AddOil方法的[Transactional]注解,并在方法中主动抛出一个错误。

1323385-20190904093605445-368599579.jpg

1323385-20190904091500533-189563059.jpg1323385-20190904091516108-1667895100.jpg

表中并未添加数据,因为事物未提交,回滚了,符合预期。

BTW,事物的开启,除了使用[Transactional]注解外,也可以通过注入uow,手动开启和提交。

3. 写在最后

        只需要在数据库实体类上注解[Table("表名")]就可以直接使用仓储了,是不是很简洁优雅呢?这里实现的仓储都是通用的,如果有特殊需求的仓储,则需要自定义接口和实现类,接口继承IRepository<T>,实现类继承BaseRepository<T>,然后注入自己的特殊仓储就行了。

        如果这篇文章对你有所帮助,不妨点个赞咯。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK