MVC - 分層式架構(一)資料存取層

  • 6219
  • 0
  • 2017-09-07

此篇記錄MVC與自己實作的基本架構。

好站推薦 :

建立程式開發框架

ASP.NET MVC 專案分層架構


要點 :

  • 把使用EF存取資料的部分從 Web 專案中抽離出來,這表示 Web 專案只關注於資料呈現以及系統控制流程的部份
  • 凡是要跟資料打交道的存取操作就不會出現在 Web 專案中,讓兩個專案有各自的職責與關注的事物
  • 該專案主要就是在操作模型的CRUD
  • 未來會再視情況新增方法或屬性,要有高使用率的才有可能會增加進來。此通用實作是針對Entity FrameWork
  • 未來所有的ORM資料庫應該在額外建立一個專案,供資料存取曾與呼叫端做使用
  • 未來會搭配ASP.NET非同步的方法

 

專案內容 : 

資料存取層/通用型 - 介面 :

using System.Linq;
using System;
using System.Linq.Expressions;

namespace Repository_Models.Interface
{
    /// <summary>
    /// 代表一個Repository的interface。
    /// </summary>
    /// <typeparam name="T">任意model的class</typeparam>
    public interface IRepository<T> where T : class
    {
        /// <summary>
        /// 新增一筆資料。
        /// </summary>
        /// <param name="entity">要新增的Entity</param>
        void Create(T entity);

        /// <summary>
        /// 刪除一筆資料內容。
        /// </summary>
        /// <param name="entity">要被刪除的Entity。</param>
        void Delete(T entity);

        /// <summary>
        /// 更新一筆資料的內容。
        /// </summary>
        /// <param name="entity">要更新的內容</param>
        void Update(T entity);

        /// <summary>
        /// 更新一筆資料的內容。只更新部分欄位。
        /// Lambda 運算式 只需要傳遞欄位屬性 EX : x => { x.ColumnName1, x.Column2 }
        /// </summary>
        /// <param name="entity">要更新的內容</param>
        /// <param name="updateProperties">需要更新的欄位。</param>
        void Update(T entity, Expression<Func<T, object>>[] updateProperties);

        /// <summary>
        /// 儲存異動。
        /// </summary>
        //void SaveChanges(); //因為實作 IunitOfWork裡面有儲存方法 因此拿掉這一方法的規範

        /// <summary>
        /// 取得第一筆符合條件的內容。如果符合條件有多筆,也只取得第一筆。
        /// </summary>
        /// <param name="predicate">要取得的Where條件。</param>
        /// <returns>取得第一筆符合條件的內容。</returns>
        T Read(Expression<Func<T, bool>> predicate);

        /// <summary>
        /// 取得合條件的內容。可能回傳一筆或是多筆
        /// </summary>
        /// <param name="predicate">要取得的Where條件。</param>
        /// <param name="predicates">要取得的Where條件。可新增N個條件值</param>
        /// <returns>只要符合條件則回傳全部筆數的IQueryable。</returns>
        IQueryable<T> Reads(Expression<Func<T, bool>> predicate,params Expression<Func<T, bool>>[] predicates);

        /// <summary>
        /// 取得Entity全部筆數的IQueryable。
        /// </summary>
        /// <returns>Entity全部筆數的IQueryable。</returns>
        IQueryable<T> Reads();
    }
}

 

資料存取層/通用型 - 實作類 :

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Data.Entity;

namespace Repository_Models.Repository
{
    //TODO : 還需把判斷 entity == null 丟出例外的 工作 拋給 Service Layer

    // internal
    /// <summary>
    /// 該類別是通用型的 Repository。使用 Entity 的機制
    /// </summary>
    /// <typeparam name="TEntity">請傳入Entity對應的資料類別</typeparam>
    internal class EFGenericRepository<TEntity> : Interface.IRepository<TEntity>
        where TEntity : class
    {
        private DbContext _context { get; set; }//使用 Entity Framework 的機制

        //public EFGenericRepository()// : this(new FinalMvcTestDatabaseEntities())
        //{
        //    throw new NotSupportedException(String.Format("{0} 不支援無參數的初始化",this.GetType().FullName));
        //}

        public EFGenericRepository(DbContext context)
        {
            this._context = context;
        }


        /// <summary>
        /// 新增一筆資料到資料庫。
        /// </summary>
        /// <param name="entity">要新增到資料的庫的Entity</param>
        public void Create(TEntity entity)
        {
            //指定entity物件為 TEntity 的類別 回傳對應的DbSet的物件 => 等同指定資料表
            this._context.Set<TEntity>().Add(entity);
        }

        /// <summary>
        /// 刪除一筆資料
        /// </summary>
        /// <param name="entity">資料對應的Entity類別</param>
        public void Delete(TEntity entity)
        {
            this._context.Entry(entity).State = EntityState.Deleted;
        }

        /// <summary>
        /// 修改一筆資料
        /// </summary>
        /// <param name="instance">資料對應的Entity類別</param>
        public void Update(TEntity entity)
        {
            this._context.Entry(entity).State = EntityState.Modified;
        }

        /// <summary>
        /// 更新一筆資料的內容。只更新部分欄位的。
        /// Lambda 運算式 只需要傳遞欄位屬性 EX : x => x.ColumnName1, x => x.Column2....
        /// </summary>
        /// <param name="entity">要更新的內容</param>
        /// <param name="updateProperties">需要更新的欄位。</param>
        public void Update(TEntity entity, Expression<Func<TEntity, object>>[] updateProperties)
        {
            // 想要略過 EF 檢查 關閉自動追蹤實體的驗證
            this._context.Configuration.ValidateOnSaveEnabled = false;
            // 其屬性還未更新到資料庫 但先做紀錄
            this._context.Entry(entity).State = EntityState.Unchanged;

            if (updateProperties != null)
            {
                // 確認那些欄位是要修改的做上記號
                foreach (var item in updateProperties)
                {
                    this._context.Entry(entity).Property(item).IsModified = true;
                }
            }
        }

        //此方法目前內部不使用 外部也不使用 先暫時放著
        /// <summary>
        /// 儲存此次的資料異動
        /// </summary>
        public void SaveChanges()
        {
            this._context.SaveChanges();

            //System.ComponentModel; System.ComponentModel.DataAnnotations; 依據實體的規範如 [[Required]]
            //因為Update(,) 單一model需要先關掉validation,因此重新打開
            if (this._context.Configuration.ValidateOnSaveEnabled == false)
                this._context.Configuration.ValidateOnSaveEnabled = true;
        }

        /// <summary>
        /// 取得第一筆符合條件的內容。如果符合條件有多筆,也只取得第一筆。
        /// </summary>
        /// <param name="predicate">要取得的Where條件。</param>
        /// <returns>取得第一筆符合條件的內容。</returns>
        public TEntity Read(Expression<Func<TEntity, bool>> predicate)
        {
            return this._context.Set<TEntity>().FirstOrDefault(predicate);
        }

        /// <summary>
        /// 取得合條件的內容。可能回傳一筆或是多筆 IQueryable
        /// </summary>
        /// <param name="predicate">要取得的Where條件。</param>
        /// <param name="predicates">要取得的Where條件。可新增N個條件值</param>
        /// <returns>只要符合條件則回傳全部筆數的IQueryable。</returns>
        public IQueryable<TEntity> Reads(Expression<Func<TEntity, bool>> predicate,
            params Expression<Func<TEntity, bool>>[] predicates)
        {
            var datas = this._context.Set<TEntity>().Where(predicate);
            if (predicates != null)
            {
                foreach (var expression in predicates)
                {
                    datas = datas.Where(expression);
                }
            }
            return datas;
        }

        /// <summary>
        /// 取得Entity全部筆數的IQueryable。
        /// </summary>
        /// <returns>Entity全部筆數的IQueryable。</returns>
        public IQueryable<TEntity> Reads()
        {
            return this._context.Set<TEntity>().AsQueryable();
        }

        #region 釋放資源
        ~EFGenericRepository()
        {
            this._context = null;
            GC.SuppressFinalize(this);
        }
        #endregion
    }
}

 

工作單元層 :

這個類有點像是工廠 - 功能為 : 
    1. 實例化 對應的 EFGenericRepository<T> 的物件給你
    2. 幫忙管控並關閉實例出來的 DbContext
    3. 需要你給相對應的Dbcontex才行 <T>需對應該 Dbcontext中的資料表
若不想要該類可以做以下修改 : 
    1. 資料存取層類別的泛型與方法中的泛型無關
    2. 想辦法使用資料存取層類別的泛型產生對應的Dbcontex

 

工作單元層-介面

using System;
using Repository_Models.Interface;

namespace IUnitOfWork.Interface
{
    /// <summary>
    /// 實作Unit Of Work的interface。
    /// </summary>
    public interface IUnitOfWork : IDisposable
    {
        /// <summary>
        /// 儲存所有異動。
        /// </summary>
        void Save();

        /// <summary>
        /// 取得某一個Entity的Repository。
        /// 如果沒有取過,會initialise一個
        /// 如果有就取得之前initialise的那個。
        /// 產生出 UnitOfWork 的物件時 可循環利用該方法達到節省記憶體空間之功能
        /// </summary>
        /// <typeparam name="T">此Context裡面的Entity Type</typeparam>
        /// <returns>Entity的Repository</returns>
        IRepository<T> GetRepository<T>() where T : class;
    }
}

 

工作單元層-實作類

using System;
using System.Collections;
using System.Data.Entity;
using Repository_Models.Interface;
using Repository_Models.Repository;

namespace IUnitOfWork
{
    /// <summary>
    /// EFGenercUnitOfWork 是統一產生對應的 EFGenericRepository 的類別 
    /// Service層 請善用該類別不應自己實例出 EFGenericRepository 的物件
    /// </summary>
    public class EFGenercUnitOfWork : Interface.IUnitOfWork
    {
        private readonly DbContext _context;

        //在生命週期結束前 使用 雜湊表 儲存 泛行方法 的 類型 當KEY 值
            //產生出的 instance 當 Value
        private Hashtable _repositories = null; 

        public EFGenercUnitOfWork(DbContext context)
        {
            if (context == null) { throw new ArgumentNullException("context"); }
            this._context = context;
        }

        /// <summary>
        /// 取得某一個Entity的Repository。
        /// 如果沒有取過,會initialise一個
        /// 如果有就取得之前initialise的那個。
        /// 產生出 UnitOfWork 的物件時 可循環利用該方法達到節省記憶體空間之功能
        /// </summary>
        /// <typeparam name="T">此Context裡面的Entity Type</typeparam>
        /// <returns>Entity的Repository</returns>
        public IRepository<T> GetRepository<T>() where T : class
        {
            if (this._repositories == null) this._repositories = new Hashtable();

            // 取得泛型中的類型型態
            var type = typeof(T).Name;

            // 雜湊表中找不到對應的類型時 創立 Key 儲存 T 類型,Value 儲存 對應的實體
            if (!this._repositories.ContainsKey(type))
            {
                // 取得通用的REPOSITORY類型
                var repositoryType = typeof(EFGenericRepository<>);

                // 使用 EFGenericRepository類型 與外部的傳遞的 泛型 建立出對應的實體,
                // 最後傳遞 EFGenericRepository 建構子所需的參數
                var repositoryInsetance = Activator.CreateInstance(repositoryType.MakeGenericType(
                    typeof(T)), this._context);

                // 儲存型別名稱 與對應的實例 到雜湊表之中
                this._repositories.Add(type, repositoryInsetance);
            }

            // 將雜湊表中的Object取出 並 轉型
            return (this._repositories[type] as IRepository<T>);
        }

        /// <summary>
        /// 儲存此次的 DbContext 資料異動
        /// </summary>
        public void Save()
        {
            //TODO: 需注意 EFGenericRepository<T> 修改方法的驗證,怕會對整體異動資料上有影響
            this._context.SaveChanges();
            if (this._context.Configuration.ValidateOnSaveEnabled == false)
                this._context.Configuration.ValidateOnSaveEnabled = true;
        }

        #region IDisposable Support
        private bool disposedValue = false; // 偵測多餘的呼叫

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                }
                this._context.Dispose();
                this._repositories.Clear();
                this._repositories = null;
                disposedValue = true;
            }
        }

        ~EFGenercUnitOfWork()
        {
            Dispose(false);
        }

        // 加入這個程式碼的目的在正確實作可處置的模式。
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }
}

 

 

註記 : 未來修改的方向

  • 資料存取層不應該針對<模型類別(Model class)>來產生實例
  • 資料存取層應針對<DbContext>來產生實例
  • 如何判斷<模型類別(Model class)>是否屬於該DbContext的呢??這應該才是關鍵

 

 


多多指教!! 歡迎交流!!

你不知道自己不知道,那你會以為你知道