Repository我的實踐

一、背景編程

  最近,有空了,想着把以前一些亂七八糟的小項目給整理一下,尤爲是涉及到Repository、UoW幾處。爲此,專門查閱了博客園中幾個大神 關於Repository的實踐,到最後都感受依然莫衷一是,因而感受這玩意兒不能深究,本身仍是緊扣Martin老爺子關於Repository及UoW的核心定義,本身實踐核心概念就是了,其餘的都不重要了。架構

二、整個項目架構app

紅框框起來的部分,就是關於Repository的那些部分,其中,Account.Infrustructure.Contract和Account.Infrusture.EF是核心,能夠跨解決方案或工程存在,前者是Repository基礎契約定義,後者是該契約基於EF的實現。接下來,分別就兩部分實現詳細說明。async

三、Repository、UoW核心實現ide

先看Repository核心契約的定義:模塊化

很簡單,一個基於netstandard的類庫,其中就兩個接口定義,分別對應Repository和UoW的核心概念,IRepository的定義以下:函數

public interface IRepository
    {
    }

    /// <summary>
    /// Repository標記接口
    /// </summary>
    public interface IRepository<TEntity> : IRepository
        where TEntity : class
    {
        IQueryable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "");

        TEntity GetByID(object id);

        void Insert(TEntity entity);

        void Delete(object id);

        void Delete(TEntity entityToDelete);

        void Update(TEntity entityToUpdate);

        void Save();
    }

非泛型空接口IRepository用來標記倉儲,是面向接口編程中很常見的作法,這個待會咱們會在使用環節進一步說明。泛型IRepository接口用來規範全部倉儲都應該具備的基礎增刪查改方法,這裏有2點須要注意:優化

1)方法返回類型爲IQueryable,目的是延遲查詢,用過相似EF的ORM的應該都知道;ui

2)接口有個泛型參數TEntity,很明顯,是要每一個實體對應一個Repository實現的未來。this

接下來再看UoW契約的定義:

public interface IUnitOfWork
    {
        DbTransaction BeginTransaction();

        void CommitTransaction();

        void RollbackTransaction();
    }

這個契約更簡單,由於我給其的職責,就只有將多個操做歸入統一事務並有效管理。這已經足夠實現Martin老爺子關於UoW的核心概念了。

以後,咱們看看IRepository、IUoW的基於EF的實現:

能夠看見,也很簡單,就是基於契約基礎工程中的兩個接口的實現,整個類庫也是基於standard的。

IUnityOfWork的實現以下:

public class EFUnitOfWork : IUnitOfWork
    {
        private readonly DbContext _context;

        public EFUnitOfWork(DbContext context)
        {
            _context = context;
        }

        public DbTransaction BeginTransaction()
        {
            _context.Database.BeginTransaction();

            return _context.Database.CurrentTransaction.GetDbTransaction();
        }

        public void CommitTransaction()
        {
            _context.SaveChanges();
            _context.Database.CommitTransaction();
        }

        public void RollbackTransaction()
        {
            _context.Database.RollbackTransaction();
        }
    }

你們注意工做單元中用到的上下文,很明顯,DBContext是基於EF的數據上下文的,並且,通常,咱們具體項目中才用到的上下文,都是SchoolDBContext之類的,那麼這裏如何註冊進來呢?若是是自定義系統服務,直接Registet<XXDbContext>().As<DbContext>()就成了(若是Autofac的話),問題是咱們注入上下文時候,是相似這樣:

services.AddDbContext<AccountContext>(options =>
                     options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));

翻遍了EF的AddDBContext的重載,也沒發現能夠註冊爲DBContext的實現啊,怎麼整。。。答案來了,這裏有個小技巧,既然咱們都明白,自定義服務是能夠註冊爲接口或基類的,那這裏咱們把XXXDBContext也當作自定義服務來註冊,你前面不是EF標準註冊了XXDBContext了麼,好,下一步,我就再把XXDBContext註冊爲DBContext,無非控制下生命週期就成,具體實現以下:

 services.AddDbContext<AccountContext>(options =>
                     options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
            services.AddScoped<DbContext>(provider => provider.GetService<AccountContext>());

上述操做是在Startup中完成的。注意,這一步比較重要,由於它直接決定了你EFUnityOfWork中是否能接收到DBContext,不這樣作,你就得在EFUnityOfWork中直接接受XXDBContext了,那還談何抽象,還談何基礎架構。。。

接下來,再看EF基礎實現中Repository的實現,以下:

public abstract class Repository<TEntity> : IRepository<TEntity>
        where TEntity : class
    {
        protected DbContext Context;
        protected DbSet<TEntity> DbSet;

        public Repository(DbContext context)
        {
            this.Context = context;
            this.DbSet = context.Set<TEntity>();
        }

        public virtual IQueryable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "")
        {
            IQueryable<TEntity> query = this.DbSet;

            if (filter != null)
            {
                query = query.Where(filter);
            }

            foreach (var includeProperty in includeProperties.Split
                (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }

            if (orderBy != null)
            {
                return orderBy(query);
            }
            else
            {
                return query;
            }
        }

        public virtual TEntity GetByID(object id)
        {
            return DbSet.Find(id);
        }

        public virtual void Insert(TEntity entity)
        {
            DbSet.Add(entity);
        }

        public virtual void Delete(object id)
        {
            TEntity entityToDelete = DbSet.Find(id);
            Delete(entityToDelete);
        }

        public virtual void Delete(TEntity entityToDelete)
        {
            if (Context.Entry(entityToDelete).State == EntityState.Detached)
            {
                DbSet.Attach(entityToDelete);
            }
            DbSet.Remove(entityToDelete);
        }

        public virtual void Update(TEntity entityToUpdate)
        {
            DbSet.Attach(entityToUpdate);
            Context.Entry(entityToUpdate).State = EntityState.Modified;
        }

        public void Save()
        {
            this.Context.SaveChanges();
        }
    }

  這個很簡單,無非就是你平時寫的直接基於XXDBContext的CRUD給抽象一下,泛型一下,而後蒸到這裏來。注意最後邊的那個save,有些實踐中會把save直接整到UoW裏邊去,我沒有,由於我對UoW的惟一指望就是,管理好事務,不涉及到事務的狀況下,應用服務層連UoW的影子都不要出現,有Repository就夠了,那就涉及到簡單CUD的保存,尤爲是像基於EF的這種實現中,還他媽必須savechanges才行。。。這裏特別說明,可能save放這裏並不合適,由於有些orm犯不着必須save才行,在非事務的狀況下,好比Dapper,再好比Chloe,因此這裏能夠更進一步優化或抽象。只能說,fuck EF,非事務性寫操做,你給我直接寫庫不就完了。。。

  你們注意,這裏既然這裏抽象出了Account.Infrustructure.Contract,以及有了Account.Infrustructure.EF的實現,以及我上邊說了那麼多各ORM關於save的不一樣,你就應該想到,抽象的目的,是爲了切換ORM準備的,假如我想切換爲Chloe的實現,那麼很簡單,改動只須要3處:

1)startup中EFDBContext的註冊改成Chole Context的註冊,如MsSqlContext;

2)ChloeUnityOfWork實現IUnitOfWork,構造函數中傳入IDbContext,下面的方法實現切換爲MsSQLContext的相關事務操做;

3)Repository中DBContext切換爲IDBContext,對應的CRUD及save切換爲基於IDBContext的實現(其實Chloe根本他媽就不須要save。。。);

上述IDbContext是Chloe的數據上下文,用過的應該清楚。另外,涉及到多ORM或切換ORM,直接更改不推薦,鍋鍋們,面向對象或者抽象的目的,不是爲了改動,而是爲了擴展,我上邊只是爲了說明要基於其餘ORM去實現,很是簡單而已,正確作法是,直接新建Account.Infrustructure.Chloe工程,而後實現兩個契約接口,跟EF的實現簡直大同小異。

 

四、應用

基礎架構定義好了,接下來就是咱們倉儲層的具體應用,這裏以一個簡單的ManifestRepository爲例看下如何實現:  

public class ManifestRepository : Repository<Manifest>, IManifestRepository
    {

        public ManifestRepository(AccountContext context)
            :base(context)
        {
        }

        public async Task<PaginatedList<Manifest>> GetManifests(DateTime start, DateTime end, int pageIndex, int pageSize)
        {
            var source = DbSet.Where(x => x.Date >= start && x.Date < new DateTime(end.Year, end.Month, end.Day).AddDays(1));
            int count = await source.CountAsync();
            List<Manifest> manifests = null;
            if (count > 0)
            {
                manifests = await source.OrderBy(x => x.Date).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
            }

            return new PaginatedList<Manifest>(pageIndex, pageSize, count, manifests ?? new List<Manifest>());
        }
    }  

 典型的,繼承基類泛型實現獲取基本CRUD方法,這裏多了一個,是由於這個查詢相對複雜,若是實際項目中,沒有這種複雜查詢,或者這種查詢只出現一次,實際上不必在ManifestRepository裏邊抽取,直接在應用服務層經過IRepository暴露的接口獲取便可。具體Repository有了,接下來咱們看應用服務層如何調用:

public class ManifestService : IManifestService
    {
        private readonly IManifestRepository _manifestRepository;
        private readonly IDailyRepository _dailyRepository;
        private readonly IUnitOfWork _unitOfWork;

        public ManifestService(IManifestRepository manifestRepository,
            IDailyRepository dailyRepository,
            IUnitOfWork unitOfWork)
        {
            _manifestRepository = manifestRepository;
            _dailyRepository = dailyRepository;
            _unitOfWork = unitOfWork;
        }

 看見沒有,典型的構造函數注入,注入 所需的倉儲,以及UoW。咱們再看看具體的一個Add方法,看下它是如何與Repository、UoW交互的:

 

 public Manifest AddManifest(Manifest manifest)
        {
            try
            {
                _unitOfWork.BeginTransaction();

                _manifestRepository.Insert(manifest);
                var daily = _dailyRepository.Get(x => x.Date.Date == manifest.Date.Date).FirstOrDefault();
                if (daily != null)
                {
                    daily.Cost += manifest.Cost;
                    _dailyRepository.Update(daily);
                }
                else
                {
                    daily = new Daily
                    {
                        ID = Guid.NewGuid().ToString(),
                        Date = manifest.Date,
                        Cost = manifest.Cost
                    };
                    _dailyRepository.Insert(daily);
                }

                _unitOfWork.CommitTransaction();

                return manifest;
            }
            catch(Exception e)
            {
                _unitOfWork.RollbackTransaction();
                throw e;
            }           
        }

   看到沒有,UoW開啓事務,而後各類倉儲原子操做,操做完畢,UoW 提交事務,或者異常出現,UoW回滾事務。是否是So easy。。。另外,假如倉儲層切換了ORM或者數據源,對應用服務層是徹底透明的,是否是so happy。。。

  另外,以前曾有園友問過,在Autofac模塊化注入中,若是不想以名字結尾來匹配,如何註冊服務或倉儲,這裏也貼出解決方案:

public class RepositoryModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterType<EFUnitOfWork>()
                .As<IUnitOfWork>()
                .InstancePerLifetimeScope();
            builder.RegisterAssemblyTypes(this.ThisAssembly)
                .Where(t => t.IsAssignableTo<IRepository>())
                .AsImplementedInterfaces()
                .InstancePerLifetimeScope();
        }
    }

 你們注意看紅色部分,這就是以前定義那個空IRepository接口的做用。記住一個詞,面向接口。。。

五、總結

  本文是針對Repository、UoW的核心概念的實現,即,Repository用於解耦應用服務層或者說叫業務邏輯層與具體數據存取,UoW用於維護事務。在此以前,曾拜讀過園子中大神們的一些文章,最終得出結論,這玩意兒,不必深究,只要抓住了Martin老爺子對兩者的核心定義,在此基礎上按照本身的理解去實踐就OK了。這玩意兒就像ML,在XX和得到GC的大前提下,採用何種姿式,各位隨意,只要本身爽就成。若是你非要嘗試各類不一樣姿式,何嘗不可,只要本身不嫌累,是否是。。。

相關文章
相關標籤/搜索