Repository個人實踐


1、背景

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

2、整個項目架構

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

3、Repository、UoW核心實現

先看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的應該都知道;

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

接下來再看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的實現簡直大同小異。

 

4、應用

基礎架構定義好了,接下來就是我們倉儲層的具體應用,這里以一個簡單的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接口的作用。記住一個詞,面向接口。。。

5、總結

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM