在前陣子,我對實體框架進行了一定的研究,然后把整個學習的過程開了一個系列,以逐步深入的方式解讀實體框架的相關技術,期間每每碰到一些新的問題需要潛入研究。本文繼續前面的主題介紹,着重從整體性的來總結一下實體框架的一些方面,希望針對這些實際問題,和大家進行學習交流。
我的整個實體框架的學習和研究,是以我的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類型的字段里去,就會報錯並提示轉換超出范圍。
解決方法如下所示:
這個問題的解決方法:
- C#代碼中 DateTime類型的字段在作為參數傳入到數據庫前記得賦值,並且的日期要大於1753年1月1日。
- C#代碼中 將原本是DateTime類型的字段修改為DateTime?類型,由於可空類型的默認值都是為null,所以傳入數據庫就可以不用賦值,數據庫中的datetime類型也是支持null值的。
- 修改數據庫中表的字段類型,將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 = "系統用戶信息報表";
最后我們來看看整個實體框架的結構和界面的效果介紹。
界面效果如下所示:
代碼結構如下所示:
架構設計的效果圖如下所示: