Entity Framework 實體框架的形成之旅--實體框架的開發的幾個經驗總結


在前陣子,我對實體框架進行了一定的研究,然后把整個學習的過程開了一個系列,以逐步深入的方式解讀實體框架的相關技術,期間每每碰到一些新的問題需要潛入研究。本文繼續前面的主題介紹,着重從整體性的來總結一下實體框架的一些方面,希望針對這些實際問題,和大家進行學習交流。

我的整個實體框架的學習和研究,是以我的Winform框架順利升級到這個實體框架基礎上為一個階段終結,這個階段事情很多,從開始客運聯網售票的WebAPI平台的開發,到微軟實體框架的深入研究,以及《基於Metronic的Bootstrap開發框架經驗總結》的主題學習和分享等等方面,都混到一起來了,多個主題之間穿插着寫一些隨筆,也是希望把自己的學習過程進行記錄總結,不用等到最后全部忘記了。

1、實體框架主鍵的類型約束問題

 在我們搭建整個實體框架的過程中,我們一般都是抽象封裝處理很多基礎的增刪改查、分頁等常見的數據處理功能,如下所示。

        /// <summary>
        /// 更新對象屬性到數據庫中
        /// </summary>
        /// <param name="t">指定的對象</param>
        /// <param name="key">主鍵的值</param>
        /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns>
        bool Update(T t, object key);

        /// <summary>
        /// 更新對象屬性到數據庫中(異步)
        /// </summary>
        /// <param name="t">指定的對象</param>
        /// <param name="key">主鍵的值</param>
        /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns>
        Task<bool> UpdateAsync(T t, object key);

        /// <summary>
        /// 根據指定對象的ID,從數據庫中刪除指定對象
        /// </summary>
        /// <param name="id">對象的ID</param>
        /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns>
        bool Delete(object id);

        /// <summary>
        /// 根據指定對象的ID,從數據庫中刪除指定對象(異步)
        /// </summary>
        /// <param name="id">對象的ID</param>
        /// <returns>執行成功返回<c>true</c>,否則為<c>false</c></returns>
        Task<bool> DeleteAsync(object id);

        /// <summary>
        /// 查詢數據庫,返回指定ID的對象
        /// </summary>
        /// <param name="id">ID主鍵的值</param>
        /// <returns>存在則返回指定的對象,否則返回Null</returns>
        T FindByID(object id);

        /// <summary>
        /// 查詢數據庫,返回指定ID的對象(異步)
        /// </summary>
        /// <param name="id">ID主鍵的值</param>
        /// <returns>存在則返回指定的對象,否則返回Null</returns>
        Task<T> FindByIDAsync(object id);

上面的外鍵統一定義為object類型,因為我們為了主鍵類型通用的考慮。

在實際上表的外鍵類型可能是很多種的,如可能是常見的字符類型,也可能是int類型,也可能是long類型等等。如果我們更新、查找、刪除整形類型的記錄的時候,那么可能機會出現錯誤:

The argument types 'Edm.Int32' and 'Edm.String' are incompatible for this operation.

這些錯誤就是主鍵類型不匹配導致的,我們操作這些接口的時候,一定要傳入對應類型給它們,才能正常的處理。

本來想嘗試在內部進行轉換處理為正確的類型的,不過沒有找到很好的解決方案來識別和處理,因此最好的解決方法,就是我們調用這些有object類型主鍵的接口時,傳入正確的類型即可。

                    RoleInfo info = CallerFactory<IRoleService>.Instance.FindByID(currentID.ToInt32());
                    if (info != null)
                    {
                        info = SetRoleInfo(info);
                        CallerFactory<IRoleService>.Instance.Update(info, info.ID);

                        RefreshTreeView();
                    }

又或者是下面的代碼:

        /// <summary>
        /// 分頁控件刪除操作
        /// </summary>
        private void winGridViewPager1_OnDeleteSelected(object sender, EventArgs e)
        {
            if (MessageDxUtil.ShowYesNoAndTips("您確定刪除選定的記錄么?") == DialogResult.No)
            {
                return;
            }

            int[] rowSelected = this.winGridViewPager1.GridView1.GetSelectedRows();
            foreach (int iRow in rowSelected)
            {
                string ID = this.winGridViewPager1.GridView1.GetRowCellDisplayText(iRow, "ID");
                CallerFactory<IDistrictService>.Instance.Delete(ID.ToInt64());
            }

            BindData();
        }

 

2、遞歸函數的處理

在很多時候,我們都會用到遞歸函數的處理,這樣能夠使得我們把整個列表的內容都合理的提取出來,是我們開發常見的知識點之一。

不過一般在處理LINQ的時候,它的遞歸函數的處理和我們普通的做法有一些差異。

例如我們如果要獲取一個樹形機構列表,如果我們指定了一個開始的機構節點ID,我們需要遞歸獲取下面的所有層次的集合的時候,常規的做法如下所示。

        /// <summary>
        /// 根據指定機構節點ID,獲取其下面所有機構列表
        /// </summary>
        /// <param name="parentId">指定機構節點ID</param>
        /// <returns></returns>
        public List<OUInfo> GetAllOUsByParent(int parentId)
        {
            List<OUInfo> list = new List<OUInfo>();
            string sql = string.Format("Select * From {0} Where Deleted <> 1 Order By PID, Name ", tableName);

            DataTable dt = SqlTable(sql);
            string sort = string.Format("{0} {1}", GetSafeFileName(sortField), isDescending ? "DESC" : "ASC");
            DataRow[] dataRows = dt.Select(string.Format(" PID = {0}", parentId), sort);
            for (int i = 0; i < dataRows.Length; i++)
            {
                string id = dataRows[i]["ID"].ToString();
                list.AddRange(GetOU(id, dt));
            }

            return list;
        }

        private List<OUInfo> GetOU(string id, DataTable dt)
        {
            List<OUInfo> list = new List<OUInfo>();

            OUInfo ouInfo = this.FindByID(id);
            list.Add(ouInfo);

            string sort = string.Format("{0} {1}", GetSafeFileName(sortField), isDescending ? "DESC" : "ASC");
            DataRow[] dChildRows = dt.Select(string.Format(" PID={0} ", id), sort);
            for (int i = 0; i < dChildRows.Length; i++)
            {
                string childId = dChildRows[i]["ID"].ToString();
                List<OUInfo> childList = GetOU(childId, dt);
                list.AddRange(childList);
            }
            return list;
        }

這里面的大概思路就是把符合條件的集合全部弄到DataTable集合里面,然后再在里面進行檢索,也就是遞歸獲取里面的內容。

上面是常規的做法,可以看出代碼量還是太多了,如果使用LINQ,就不需要這樣了,而且也不能這樣處理。

使用實體框架后,主要就是利用LINQ進行一些集合的操作,這些LINQ的操作雖然有點難度,不過學習清楚了,處理起來也是比較方便的。

在數據訪問層,處理上面同等的功能,LINQ操作代碼如下所示。

        /// <summary>
        /// 根據指定機構節點ID,獲取其下面所有機構列表
        /// </summary>
        /// <param name="parentId">指定機構節點ID</param>
        /// <returns></returns>
        public IList<Ou> GetAllOUsByParent(int parentId)
        {
            //遞歸獲取指定PID及下面所有所有的OU
            var query = this.GetQueryable().Where(s => s.PID == parentId).Where(s => !s.Deleted.HasValue || s.Deleted == 0).OrderBy(s => s.PID).OrderBy(s => s.Name);
            return query.ToList().Concat(query.ToList().SelectMany(t => GetAllOUsByParent(t.ID))).ToList();
        }

基本上,可以看到就是兩行代碼了,是不是很神奇,它們實現的功能完全一致。

不過,也不是所有的LINQ遞歸函數都可以做的非常簡化,有些遞歸函數,我們還是需要使用常規的思路進行處理。

        /// <summary>
        /// 獲取樹形結構的機構列表
        /// </summary>
        public IList<OuNodeInfo> GetTree()
        {
            IList<OuNodeInfo> returnList = new List<OuNodeInfo>();
            IList<Ou> list = this.GetQueryable().Where(p => p.PID == -1).OrderBy(s => s.PID).OrderBy(s => s.Name).ToList();

            if (list != null)
            {
                foreach (Ou info in list.Where(s => s.PID == -1))
                {
                    OuNodeInfo nodeInfo = GetNode(info);
                    returnList.Add(nodeInfo);
                }
            }
            return returnList;
        }

不過相對來說,LINQ已經給我們帶來的非常大的便利了。

 

3、日期字段類型轉換的錯誤處理

我們在做一些表的時候,一般情況下都會有日期類型存在,如我們的生日,創建、編輯日期等,一般我們數據庫可能用的是datetime類型,如果這個日期的類型內容在下面這個區間的話:

"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)

我們可能就會得到下面的錯誤:

從 datetime2 數據類型到 datetime 數據類型的轉換產生一個超出范圍的值

一般之所以會報錯數據類型轉換產生一個超出范圍的值,都是因為數據的大小和范圍超出要轉換的目標的原因。我們先看datetime2和datetime這兩個數據類型的具體區別在哪里。

官方MSDN對於datetime2的說明:定義結合了 24 小時制時間的日期。 可將 datetime2 視作現有 datetime 類型的擴展,其數據范圍更大,默認的小數精度更高,並具有可選的用戶定義的精度。

這里值的注意的是datetime2的日期范圍是"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)。而datetime的日期范圍是:”1753 年 1 月 1 日到 9999 年 12 月 31 日“。這里的日期范圍就是造成“從 datetime2 數據類型到 datetime 數據類型的轉換產生一個超出范圍的值”這個錯誤的原因!!!

在c#中,如果實體類的屬性沒有賦值,一般都會取默認值,比如int類型的默認值為0,string類型默認值為null, 那DateTime的默認值呢?由於DateTime的默認值為"0001-01-01",所以entity framework在進行數據庫操作的時候,在傳入數據的時會自動將原本是datetime類型的數據字段轉換為datetime2類型(因為0001-01-01這個時間超出了數據庫中datetime的最小日期范圍),然后在進行數據庫操作。問題來了,雖然EF已經把要保存的數據自動轉為了datetime2類型,但是數據庫中表的字段還是datetime類型!所以將datetime2類型的數據添加到數據庫中datetime類型的字段里去,就會報錯並提示轉換超出范圍。

解決方法如下所示:

這個問題的解決方法:

  1. C#代碼中 DateTime類型的字段在作為參數傳入到數據庫前記得賦值,並且的日期要大於1753年1月1日。
  2. C#代碼中 將原本是DateTime類型的字段修改為DateTime?類型,由於可空類型的默認值都是為null,所以傳入數據庫就可以不用賦值,數據庫中的datetime類型也是支持null值的。
  3. 修改數據庫中表的字段類型,將datetime類型修改為datetime2類型

例如,我在實體框架里面,對用戶表的日期類型字段進行初始化,這樣就能保證我存儲數據的時候,默認值是不會有問題的。

    /// <summary>
    /// 系統用戶信息,數據實體對象
    /// </summary>
    public class User
    { 
        /// <summary>
        /// 默認構造函數(需要初始化屬性的在此處理)
        /// </summary>
        public User()
        {
            this.ID= 0;

            //從 datetime2 數據類型到 datetime 數據類型的轉換產生一個超出范圍的值
            //避免這個問題,可以初始化日期字段
            DateTime defaultDate = Convert.ToDateTime("1900-1-1");
            this.Birthday = defaultDate;
            this.LastLoginTime = defaultDate;
            this.LastPasswordTime = defaultDate;
            this.CurrentLoginTime = defaultDate;

            this.EditTime = DateTime.Now;
            this.CreateTime = DateTime.Now;
         }

有時候,雖然這樣設置了,但是在界面可能給這個日期字段設置了不合理的值,也可能產生問題。那么我們對於這種情況,判斷一下,如果小於某個值,我們給它一個默認值。

 

4、實體框架的界面處理

在界面調整這塊,我們還是盡可能保持着的Enterprise Library的Winform界面樣式,也就是混合型或者普通Winform的界面效果。不過這里我們是以混合式框架進行整合測試,因此實體框架的各個方面的調用處理基本上保持一致。

不過由於實體框架里面,實體類避免耦合的原因,我們引入了DTO的概念,並使用了AutoMapper組件進行了Entity與DTO的相互映射,具體介紹可以參考《Entity Framework 實體框架的形成之旅--數據傳輸模型DTO和實體模型Entity的分離與聯合

》。

因此我們在界面操作的都是DTO對象類型了,我們在定義的時候,為了避免更多的改動,依舊使用***Info這樣的類名稱作為DTO對象的名稱,***代表表名對象。

在混合式框架的界面表現層,它們的數據對象的處理基本上保持和原來的代碼差不多。

        /// <summary>
        /// 新增狀態下的數據保存
        /// </summary>
        /// <returns></returns>
        public override bool SaveAddNew()
        {
            UserInfo info = tempInfo;//必須使用存在的局部變量,因為部分信息可能被附件使用
 SetInfo(info);
            info.Creator = Portal.gc.UserInfo.FullName;
            info.Creator_ID = Portal.gc.UserInfo.ID.ToString();
            info.CreateTime = DateTime.Now;

            try
            {
                #region 新增數據

                bool succeed = CallerFactory<IUserService>.Instance.Insert(info); if (succeed)
                {
                    //可添加其他關聯操作

                    return true;
                }
                #endregion
            }
            catch (Exception ex)
            {
                LogTextHelper.Error(ex);
                MessageDxUtil.ShowError(ex.Message);
            }
            return false;
        }

但我們需要在WCF服務層說明他們之間的映射關系,方便進行內部的轉換處理。

在實體框架界面層的查詢中,我們也不在使用部分SQL的條件做法了,采用更加安全的基於DTO的LINQ表達式進行封裝,最后傳遞給后台的也就是一個LINQ對象(非傳統方式的實體LINQ,那樣在分布式處理中會出錯)。

如查詢條件的封裝處理如下所示:

        /// <summary>
        /// 根據查詢條件構造查詢語句
        /// </summary> 
        private ExpressionNode GetConditionSql()
        {
            Expression<Func<UserInfo, bool>> expression = p => true;
            if (!string.IsNullOrEmpty(this.txtHandNo.Text))
            {
                expression = expression.And(x => x.HandNo.Equals(this.txtHandNo.Text));
            }
            if (!string.IsNullOrEmpty(this.txtName.Text))
            {
                expression = expression.And(x => x.Name.Contains(this.txtName.Text));
            }
.........................................

            //如果是公司管理員,增加公司標識
            if (Portal.gc.UserInRole(RoleInfo.CompanyAdminName))
            {
                expression = expression.And(x => x.Company_ID == Portal.gc.UserInfo.Company_ID);
            }

            //如果是單擊節點得到的條件,則使用樹列表的,否則使用查詢條件的
            if (treeCondition != null)
            {
                expression = treeCondition;
            }

            //如非選定,只顯示正常用戶
            if (!this.chkIncludeDelete.Checked)
            {
                expression = expression.And(x => x.Deleted == 0);
            }
            return expression.ToExpressionNode();
        }

而分頁查詢的處理,依舊和原來的風格差不多,只不過這里的Where條件為ExpressionNode 對象了,如代碼所示、

            ExpressionNode where = GetConditionSql();
            PagerInfo PagerInfo = this.winGridViewPager1.PagerInfo;
            IList<UserInfo> list = CallerFactory<IUserService>.Instance.FindWithPager(where, ref PagerInfo);
            this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<UserInfo>(list);
            this.winGridViewPager1.PrintTitle = "系統用戶信息報表";

最后我們來看看整個實體框架的結構和界面的效果介紹。

界面效果如下所示:

代碼結構如下所示:

架構設計的效果圖如下所示:

 


免責聲明!

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



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