天天看點

Repository模式與UnitOfWorks模式的運用

面對資料通路環境的多樣性和複雜性,靈活地運用好Repository模式是一個很好的選擇。經過長期的應用實踐,我對Repository和UnitOfWork又有了新的體會與感悟,在新的了解上我對其運用加入了一些優化的方式與方法。

軟體開發就像是一個江湖,而設計模式就是一本高深的秘籍每讀一次、用一次、想一次都能得到新的領悟,讓我們的設計技能有所提高。開始時我們可能會“為了模式而模式”,讓代碼變得亂78糟甚至難以讓人了解,但随着設計能力的提高與模式的運用和了解,慢慢地我們可能會忘掉模式随心所欲,此時再讀讀代碼或者你已經發現自己的工程已融合模式之美—"模式為設計而生,設計為需求而活"。在開篇突然想分享一下這10幾年用模式的一點小小的領悟。

IRepository 與 IUnitOfWork

在2011年我曾在Codeproject 發表過題為 "The Repository Pattern with EF Code First & Dependency Injection in ASP.NET MVC3" 的文章,當時講述的是Repository模式和Ioc如何結合EF在ASP.NET MVC3上應用。曆時3年多對Repository的使用,最近又有新的感悟。 Repository 是這幾年我用得最多的模式之一,而且也是我認為最常用和最容易掌握的一種模式,我使用Repository的目的有二:

  • 将資料的執行個體化解耦,通過Ioc或是工廠方法構造資料對象而不是用 "new"
  • 将資料的邏輯行為(CURD)解耦,這樣會便于我更換任何形式的資料庫,無論底層資料庫使用的是MSSQL還是MongoDB我都可以采用相同的通路邏輯。

以下是我期望在代碼中看到的效果

public class PostController:Contrller
{
   private IRepository repository;
   
   public MyController(IRepository repository){
      this.repository=repository;
   }

   public Action Get(int id) {
     return View(repository.Find(id));
   }

   [HttpPost]
   public Action Create(Post post) {
     repository.Add(post);
     repository.Submit();
     return this.Get(post.ID);
   } 

   ...
}      

這裡使用了構造注入,由MVC向Controller注入Repository的執行個體,這樣我就不需要在使Repository的時候去建立它了。(這種例子你Google會發現很多,包括在asp.net上也不少)

單一地使用Repository很容易了解,但運用在項目中的執行個體情況可以這樣嗎?大多數答案是否定的,如果目前的Controller控制的不單純是一個實體,而是多個實體時,如果使用純Repository的話代碼會變糟:

public class PostController:Contrller
{
   private IRepository posts;
   private IRepository blogs;
   private IRepository owners;

   public MyController(IRepository blogs,IRepository posts,IRepository owners){
      this.posts=posts;
      this.blogs=blogs;
      this.owners=owners;
   }

   public Action Get(int id) {
     var post=posts.Find(id)
     var blog=blogs.Find(post.BlogID);
     var owner=posts.Find(post.Owner);
     ...
     return View(new { Post=post, Blog=blog, Owner=owner });
   }


   ...
}      

一看上去似乎沒什麼大問題,隻是在構造時多了兩個Repository的注入,但是我們的目隻有一個Controller嗎? 而每個實體都需要去實作一個Repository? 這樣的寫法在項目中散播會怎麼樣呢?

最後你會得到一大堆雞肋式的"Repository",他們的相似度非常大。或是你使用一個Repository實作處理所有的實體,但你需要在構造時進行配置 (注:構造注入并不代表不構造,而是将構造代碼放在了一個統一的地方),更糟的情況是,如果使用的是繼承于IRepository的子接口,那麼Controller就會與IRepository的耦合度加大。

很明顯 Repository 不是一種獨立的模式,它自身和其它模式有很強的相關度,如果隻是拿它來獨立使用局限性會很大。我們需要其它的接口去統一“管理”Repository和避免在Controller内顯式的出現Repository的類型聲明。将上面的代碼改一下:

public class PostController:Contrller
{
   private IUnitOfWorks works;
   public PostController(IUnitOfWorks works){
     this.works=works;
   }
   public Action Get(int id) {
     var post=works.Posts.Find(id)
     var blog=works.Blogs.Find(post.BlogID);
     var owner=works.Posts.Find(post.Owner);
     ...
     return View(new { Post=post, Blog=blog, Owner=owner });
   }

   [HttpPost]
   public Action Create(Post post)
   {
       works.Add(post);
       ...
    }
}      

這裡就引入了 UnitOfWorks 模式,将對所有的Repository的耦合消除(把所有的Repository變量的聲明去掉了),

IRepository與IUnitOfWorks兩個模式是最佳組合,我們可以通過對UnitOfWorks一個類進行IoC處理,而調用方代碼則集中從IUnitOfWorks對象擷取所需的Repository,接下來看看他們的定義吧

IRepository 接口

/// <summary>
    /// 定義通用的Repository接口
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IRepository<T>: IDisposable
        where T : class
    {
        /// <summary>
        /// 擷取所有的實體對象
        /// </summary>
        /// <returns></returns>
        IQueryable<T> All();

/// <summary>
        /// 通過Lamda表達式過濾符合條件的實體對象
        /// </summary>
        IQueryable<T> Filter(Expression<Func<T, bool>> predicate);
/// <summary>
        /// Gets the object(s) is exists in database by specified filter.
        /// </summary>
        bool Contains(Expression<Func<T, bool>> predicate);

        /// <summary>
        /// 擷取實體總數
        /// </summary>
        int Count();

        int Count(Expression<Func<T, bool>> predicate);

        /// <summary>
        /// 通過鍵值查找并傳回單個實體
        /// </summary>
        T Find(params object[] keys);

        /// <summary>
        /// 通過表達式查找複合條件的單個實體
        /// </summary>
        /// <param name="predicate"></param>
        T Find(Expression<Func<T, bool>> predicate);

        /// <summary>
        /// 建立實體對象
        /// </summary>
        T Create(T t);

        /// <summary>
        /// 删除實體對象
        /// </summary>
        void Delete(T t);

        /// <summary>
        /// 删除符合條件的多個實體對象
        /// </summary>
        int Delete(Expression<Func<T, bool>> predicate);

        /// <summary>
        /// Update object changes and save to database.
        /// </summary>
        /// <param name="t">Specified the object to save.</param>
        T Update(T t);
        
        /// <summary>
        /// Clear all data items.
        /// </summary>
        /// <returns>Total clear item count</returns>
        void Clear();

        /// <summary>
        /// Save all changes.
        /// </summary>
        /// <returns></returns>
        int Submit();
    }      

 IUnitOfWorks 接口

public interface IUnitOfWorks
    {
        IQueryable<T> Where<T>(Expression<Func<T, bool>> predicate) where T : class;

        IQueryable<T> All<T>() where T : class;

int Count<T>() where T : class;

        int Count<T>(Expression<Func<T, bool>> predicate) where T : class;

        T Find<T>(object id) where T : class;

        T Find<T>(Expression<Func<T, bool>> predicate) where T : class;

        T Add<T>(T t) where T : class;

        IEnumerable<T> Add<T>(IEnumerable<T> items) where T : class;

        void Update<T>(T t) where T : class;

        void Delete<T>(T t) where T : class;

        void Delete<T>(Expression<Func<T, bool>> predicate) where T : class;

        void Clear<T>() where T : class;

        int SaveChanges();

        void Config(IConfiguration settings);
    }      

仔細一看你會發現IRepository和IUnitOfWorks的定義非常相似,在使用的角度是UnitOfWorks就是所有執行個體化的Repository的一個統一“包裝”, 這是我在發表 "The Repository Pattern with EF Code First & Dependency Injection in ASP.NET MVC3" 後對IUnitOfWorks在實用上的一種擴充。以前的方式是直接在UnitOfWorks的屬性的中暴露一個Repository執行個體給外部使用,但這樣做的話會降低IUnitOfWorks的通用性。是以我讓IUnitOfWorks使用起來更像是一個IRepository.看一段代碼的比較:

//之前的做法,Posts是一個IRepository的實作,是不是很像EF
var post=works.Posts.Add(new Post()); //C
works.Posts.Update(post);//U
var post=works.Posts.Get(id); //R
works.Posts.Delete(post);//D

//優化後的IUnitOfWorks
var post=works.Add(new Post());//C
works.Update(post);//U
var post=works.Get(id); //R
works.Delete(post);//D      

如果将IRepository通過屬性的方式暴露給調用方,IUnitOfWorks的擴充性就會下降,而且會令IUnitOfWorks的實作類與調用方建立很緊密的耦合。我對IUnitOfWorks優化後以泛型決定使用哪一個Repository,這樣可以将IUnitOfWorks與調用方進行解耦。所有的實體通過一個通用Repository實作,這樣可以避免為每一個實體寫一個Repository。而對于具有特殊處理邏輯的Repository才通過屬性暴露給調用方。

IRepository 與 IUnitOfWorks的實作

在這裡我會先實作一套使用EF通路資料庫的通用 Repository 和 UnitOfWorks

EntityRepository 

public class EntityRepository<TContext, TObject> : IRepository<TObject>
        where TContext : DbContext
        where TObject : class
    {
        protected TContext context;
        protected DbSet<TObject> dbSet;
        protected bool IsOwnContext = false;

        /// <summary>
        /// Gets the data context object.
        /// </summary>
        protected virtual TContext Context { get { return context; } }

        /// <summary>
        /// Gets the current DbSet object.
        /// </summary>
        protected virtual DbSet<TObject> DbSet { get { return dbSet; } }

        /// <summary>
        /// Dispose the class.
        /// </summary>
        public void Dispose()
        {
            if ((IsOwnContext) && (Context != null))
                Context.Dispose();
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Get all objects.
        /// </summary>
        /// <returns></returns>
        public virtual IQueryable<TObject> All()
        {
            return  DbSet.AsQueryable();
        }

/// <summary>
        /// Gets objects by specified predicate.
        /// </summary>
        /// <param name="predicate">The predicate object.</param>
        /// <returns>return an object collection result.</returns>
        public virtual IQueryable<TObject> Filter(Expression<Func<TObject, bool>> predicate)
        {
            return  DbSet.Where(predicate).AsQueryable<TObject>();
        }

public bool Contains(Expression<Func<TObject, bool>> predicate)
        {
            return DbSet.Count(predicate) > 0;
        }

        /// <summary>
        /// Find object by keys.
        /// </summary>
        /// <param name="keys"></param>
        /// <returns></returns>
        public virtual TObject Find(params object[] keys)
        {
            return DbSet.Find(keys);
        }

        public virtual TObject Find(Expression<Func<TObject, bool>> predicate)
        {
            return DbSet.FirstOrDefault(predicate);
        }

        public virtual TObject Create(TObject TObject)
        {
            var newEntry = DbSet.Add(TObject);
            if (IsOwnContext)
                Context.SaveChanges();
            return newEntry;
        }

        public virtual void Delete(TObject TObject)
        {
            var entry = Context.Entry(TObject);
            DbSet.Remove(TObject);
            if (IsOwnContext)
                Context.SaveChanges();
        }

        public virtual TObject Update(TObject TObject)
        {
            var entry = Context.Entry(TObject);
            DbSet.Attach(TObject);
            entry.State = EntityState.Modified;
            if (IsOwnContext)
                Context.SaveChanges();
            return TObject;
        }

        public virtual int Delete(Expression<Func<TObject, bool>> predicate)
        {
            var objects = DbSet.Where(predicate).ToList();
            foreach (var obj in objects)
                DbSet.Remove(obj);
            
            if (IsOwnContext)
                return Context.SaveChanges();
            return objects.Count();
        }

        public virtual int Count()
        {
            return DbSet.Count();
        }

        public virtual int Count(Expression<Func<TObject, bool>> predicate)
        {
            return DbSet.Count(predicate);
        }

        public int Submit()
        {
            return Context.SaveChanges();
        }     

        public virtual void Clear()
        {
             
        }
    }      

 UnitOfWorks

public class UnitOfWorks<TDBContext> : IUnitOfWorks
        where TDBContext :DbContext
    {
        protected TDBContext dbContext;

        public UnitOfWorks<TDBContext>(TDBContext context)
        {
             dbContext=context;
        }

        //構造通用的Repository
        private IDictionary<Type,object> repositoryTable = new Dictionary<Type,object>();
        
        //注冊其它的Repository
        public void Register<T>(IRepository<T> repository)
        {
           var key=typeof(T);
           if (repositoryTable.ContainsKey(key))
              repositoryTable[key].Add(repository);
        }


        private IRepository<T> GetRepository<T>()
            where T:class
        {
            IRepository<T> repository = null;
            var key=typeof(T);
            
            if (repositoryTable.ContainsKey(key))
                repository = (IRepository<T>)repositoryTable[key];
            else
            {
                repository = GenericRepository<T>();
                repositoryTable.Add(key, repository);
            }

            return repository;
        }

        protected virtual IRepository<T> GenericRepository<T>() where T : class
        {
            return new EntityRepository<T>(dbContext);
        }

        public T Find<T>(object id) where T : class
        {
            return GetRepository<T>().Find(id);
        }

        public T Add<T>(T t) where T : class
        {
            return GetRepository<T>().Create(t);
        }

        public IEnumerable<T> Add<T>(IEnumerable<T> items) where T : class
        {
            var list = new List<T>();
            foreach (var item in items)
                list.Add(Add(item));
            return list;
        }

        public void Update<T>(T t) where T : class
        {
            GetRepository<T>().Update(t);
        }

        public void Delete<T>(T t) where T : class
        {
            GetRepository<T>().Delete(t);
        }

        public void Delete<T>(Expression<Func<T, bool>> predicate) where T : class
        {
            GetRepository<T>().Delete(predicate);
        }

        public int SaveChanges(bool validateOnSave = true)
        {
            if (!validateOnSave)
                dbContext.Configuration.ValidateOnSaveEnabled = false;

            return dbContext.SaveChanges();
        }

        public void Dispose()
        {
            if (dbContext != null)
                dbContext.Dispose();
            GC.SuppressFinalize(this);
        }
        
        public System.Linq.IQueryable<T> Where<T>(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
            where T:class
        {
            return GetRepository<T>().Filter(predicate);
        }

        public T Find<T>(System.Linq.Expressions.Expression<Func<T, bool>> predicate) where T : class
        {
            return GetRepository<T>().Find(predicate);
        }

        public System.Linq.IQueryable<T> All<T>() where T : class
        {
            return GetRepository<T>().All();
        }

        public int Count<T>() where T : class
        {
            return GetRepository<T>().Count();
        }

        public int Count<T>(System.Linq.Expressions.Expression<Func<T, bool>> predicate) where T : class
        {
            return GetRepository<T>().Count(predicate);
        }

        public void Config(IConfiguration settings)
        { 
            var configuration=settings as DbConfiguration ;
            if (configuration != null)
            {
                this.dbContext.Configuration.AutoDetectChangesEnabled = configuration.AutoDetectChangesEnabled;
                this.dbContext.Configuration.LazyLoadingEnabled = configuration.LazyLoadingEnabled;
                this.dbContext.Configuration.ProxyCreationEnabled = configuration.ProxyCreationEnabled;
                this.dbContext.Configuration.ValidateOnSaveEnabled = configuration.ValidateOnSaveEnabled;
            }
        }

        public void Clear<T>() where T : class
        {
            GetRepository<T>().Clear();
        }

        int IUnitOfWorks.SaveChanges()
        {
            return this.SaveChanges();
        }
}      

 接下來看看如何使用這套基于EF的實作,首先是對Model的定義

public class Category
    {
        [Key]
        public int ID { get; set; }

        public virtual string Name { get; set; }

        public virtual string Title { get; set; }

        public virtual ICollection<Product> Products { get; set; }
    }

    public class Product
    {
        [Key]
        public int ID { get; set; }
        
        public int CategoryID { get; set; }

        [ForeignKey("CategoryID")]
        public virtual Category Category {get;set;}

        public string Name { get; set; }

        public string Title { get; set; }

        public string Description{get;set;}

        public decimal Price { get; set; }
    }

    public class DB : DbContext
    {
        public DB() : base("DemoDB") { }
        public DbSet<Category> Categories { get; set; }
        public DbSet<Product> Products { get; set; }
    }      

調用方:使用UnitOfWorks和Repository

var works=new UnitOfWorks(new DB());

var pc=works.Add(new Category()
{  
   Name="PC",
   Title="電腦"
});

workds.Add(new Product(){
   Category=pc,
   Name="iMac",
   Title="iMac"
   Price=9980
})

works.SaveChanges();      

 注:如果需要使用IoC方式構造UnitOfWorks 可參考我在 "The Repository Pattern with EF Code First & Dependency Injection in ASP.NET MVC3" 一文中提及如何在MVC内通過Unity 實作DI.

小結

 以上述例子為例,如果我們想将Category存儲于文本檔案而不想改動調用方的代碼。我們可以實作一個 FileBaseCategoryRepository,然後在UnitOfWorks在構造後調用Register方法将預設的Category Repository替換掉,同理,這就可以建立不同的Repository去配置UnitOfWork

var works=new UnitOfWorks(new DB());
works.Register(new FileBaseCategoryRepository());

var pc=works.Add(new Category()
{  
   Name="PC",
   Title="電腦"
});

workds.Add(new Product(){
   Category=pc,
   Name="iMac",
   Title="iMac"
   Price=9980
})

works.SaveChanges();      

使用IUnitOfWorks+IRepository模式你就可以靈活地配置你的資料通路方式,可以通過Repository極大地提通實體存儲邏輯代碼的重用性。

 相關參考

  • Repository Pattern - by Martin fowler
  • The Repository Pattern with EF Code First & Dependency Injection in ASP.NET MVC3 - by Ray

繼續閱讀