Entity Framework 實(shí)體框架的形成之旅--實(shí)體框架的開發(fā)的幾個(gè)經(jīng)驗(yàn)總結(jié)

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

我的整個(gè)實(shí)體框架的學(xué)習(xí)和研究,是以我的Winform框架順利升級(jí)到這個(gè)實(shí)體框架基礎(chǔ)上為一個(gè)階段終結(jié),這個(gè)階段事情很多,從開始客運(yùn)聯(lián)網(wǎng)售票的WebAPI平臺(tái)的開發(fā),到微軟實(shí)體框架的深入研究,以及《基于Metronic的Bootstrap開發(fā)框架經(jīng)驗(yàn)總結(jié)》的主題學(xué)習(xí)和分享等等方面,都混到一起來了,多個(gè)主題之間穿插著寫一些隨筆,也是希望把自己的學(xué)習(xí)過程進(jìn)行記錄總結(jié),不用等到最后全部忘記了。

1、實(shí)體框架主鍵的類型約束問題

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

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

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

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

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

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

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

上面的外鍵統(tǒng)一定義為object類型,因?yàn)槲覀優(yōu)榱酥麈I類型通用的考慮。

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

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

這些錯(cuò)誤就是主鍵類型不匹配導(dǎo)致的,我們操作這些接口的時(shí)候,一定要傳入對(duì)應(yīng)類型給它們,才能正常的處理。

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

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

    RefreshTreeView();
}

又或者是下面的代碼:

/// <summary>
/// 分頁(yè)控件刪除操作
/// </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、遞歸函數(shù)的處理

在很多時(shí)候,我們都會(huì)用到遞歸函數(shù)的處理,這樣能夠使得我們把整個(gè)列表的內(nèi)容都合理的提取出來,是我們開發(fā)常見的知識(shí)點(diǎn)之一。

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

例如我們?nèi)绻@取一個(gè)樹形機(jī)構(gòu)列表,如果我們指定了一個(gè)開始的機(jī)構(gòu)節(jié)點(diǎn)ID,我們需要遞歸獲取下面的所有層次的集合的時(shí)候,常規(guī)的做法如下所示。

/// <summary>
/// 根據(jù)指定機(jī)構(gòu)節(jié)點(diǎn)ID,獲取其下面所有機(jī)構(gòu)列表
/// </summary>
/// <param name="parentId">指定機(jī)構(gòu)節(jié)點(diǎn)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集合里面,然后再在里面進(jìn)行檢索,也就是遞歸獲取里面的內(nèi)容。

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

使用實(shí)體框架后,主要就是利用LINQ進(jìn)行一些集合的操作,這些LINQ的操作雖然有點(diǎn)難度,不過學(xué)習(xí)清楚了,處理起來也是比較方便的。

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

/// <summary>
/// 根據(jù)指定機(jī)構(gòu)節(jié)點(diǎn)ID,獲取其下面所有機(jī)構(gòu)列表
/// </summary>
/// <param name="parentId">指定機(jī)構(gòu)節(jié)點(diǎn)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();
}

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

不過,也不是所有的LINQ遞歸函數(shù)都可以做的非常簡(jiǎn)化,有些遞歸函數(shù),我們還是需要使用常規(guī)的思路進(jìn)行處理。

/// <summary>
/// 獲取樹形結(jié)構(gòu)的機(jī)構(gòu)列表
/// </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;
}

不過相對(duì)來說,LINQ已經(jīng)給我們帶來的非常大的便利了。

3、日期字段類型轉(zhuǎn)換的錯(cuò)誤處理

我們?cè)谧鲆恍┍淼臅r(shí)候,一般情況下都會(huì)有日期類型存在,如我們的生日,創(chuàng)建、編輯日期等,一般我們數(shù)據(jù)庫(kù)可能用的是datetime類型,如果這個(gè)日期的類型內(nèi)容在下面這個(gè)區(qū)間的話:

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

我們可能就會(huì)得到下面的錯(cuò)誤:

從 datetime2 數(shù)據(jù)類型到 datetime 數(shù)據(jù)類型的轉(zhuǎn)換產(chǎn)生一個(gè)超出范圍的值

一般之所以會(huì)報(bào)錯(cuò)數(shù)據(jù)類型轉(zhuǎn)換產(chǎn)生一個(gè)超出范圍的值,都是因?yàn)閿?shù)據(jù)的大小和范圍超出要轉(zhuǎn)換的目標(biāo)的原因。我們先看datetime2和datetime這兩個(gè)數(shù)據(jù)類型的具體區(qū)別在哪里。
官方MSDN對(duì)于datetime2的說明:定義結(jié)合了 24 小時(shí)制時(shí)間的日期。 可將 datetime2 視作現(xiàn)有 datetime 類型的擴(kuò)展,其數(shù)據(jù)范圍更大,默認(rèn)的小數(shù)精度更高,并具有可選的用戶定義的精度。
這里值的注意的是datetime2的日期范圍是"0001-01-01 到 9999-12-31"(公元元年 1 月 1 日到公元 9999 年 12 月 31 日)。而datetime的日期范圍是:”1753 年 1 月 1 日到 9999 年 12 月 31 日“。這里的日期范圍就是造成“從 datetime2 數(shù)據(jù)類型到 datetime 數(shù)據(jù)類型的轉(zhuǎn)換產(chǎn)生一個(gè)超出范圍的值”這個(gè)錯(cuò)誤的原因!??!
在c#中,如果實(shí)體類的屬性沒有賦值,一般都會(huì)取默認(rèn)值,比如int類型的默認(rèn)值為0,string類型默認(rèn)值為null, 那DateTime的默認(rèn)值呢?由于DateTime的默認(rèn)值為"0001-01-01",所以entity framework在進(jìn)行數(shù)據(jù)庫(kù)操作的時(shí)候,在傳入數(shù)據(jù)的時(shí)會(huì)自動(dòng)將原本是datetime類型的數(shù)據(jù)字段轉(zhuǎn)換為datetime2類型(因?yàn)?001-01-01這個(gè)時(shí)間超出了數(shù)據(jù)庫(kù)中datetime的最小日期范圍),然后在進(jìn)行數(shù)據(jù)庫(kù)操作。問題來了,雖然EF已經(jīng)把要保存的數(shù)據(jù)自動(dòng)轉(zhuǎn)為了datetime2類型,但是數(shù)據(jù)庫(kù)中表的字段還是datetime類型!所以將datetime2類型的數(shù)據(jù)添加到數(shù)據(jù)庫(kù)中datetime類型的字段里去,就會(huì)報(bào)錯(cuò)并提示轉(zhuǎn)換超出范圍。
解決方法如下所示:
這個(gè)問題的解決方法:
C#代碼中 DateTime類型的字段在作為參數(shù)傳入到數(shù)據(jù)庫(kù)前記得賦值,并且的日期要大于1753年1月1日。
C#代碼中 將原本是DateTime類型的字段修改為DateTime?類型,由于可空類型的默認(rèn)值都是為null,所以傳入數(shù)據(jù)庫(kù)就可以不用賦值,數(shù)據(jù)庫(kù)中的datetime類型也是支持null值的。
修改數(shù)據(jù)庫(kù)中表的字段類型,將datetime類型修改為datetime2類型

例如,我在實(shí)體框架里面,對(duì)用戶表的日期類型字段進(jìn)行初始化,這樣就能保證我存儲(chǔ)數(shù)據(jù)的時(shí)候,默認(rèn)值是不會(huì)有問題的。

/// <summary>
/// 系統(tǒng)用戶信息,數(shù)據(jù)實(shí)體對(duì)象
/// </summary>
public class User
{ 
    /// <summary>
    /// 默認(rèn)構(gòu)造函數(shù)(需要初始化屬性的在此處理)
    /// </summary>
    public User()
    {
        this.ID= 0;

        //從 datetime2 數(shù)據(jù)類型到 datetime 數(shù)據(jù)類型的轉(zhuǎn)換產(chǎn)生一個(gè)超出范圍的值
        //避免這個(gè)問題,可以初始化日期字段
        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;
     }

有時(shí)候,雖然這樣設(shè)置了,但是在界面可能給這個(gè)日期字段設(shè)置了不合理的值,也可能產(chǎn)生問題。那么我們對(duì)于這種情況,判斷一下,如果小于某個(gè)值,我們給它一個(gè)默認(rèn)值。


4、實(shí)體框架的界面處理

在界面調(diào)整這塊,我們還是盡可能保持著的Enterprise Library的Winform界面樣式,也就是混合型或者普通Winform的界面效果。不過這里我們是以混合式框架進(jìn)行整合測(cè)試,因此實(shí)體框架的各個(gè)方面的調(diào)用處理基本上保持一致。
不過由于實(shí)體框架里面,實(shí)體類避免耦合的原因,我們引入了DTO的概念,并使用了AutoMapper組件進(jìn)行了Entity與DTO的相互映射,具體介紹可以參考《Entity Framework 實(shí)體框架的形成之旅--數(shù)據(jù)傳輸模型DTO和實(shí)體模型Entity的分離與聯(lián)合
》。


因此我們?cè)诮缑娌僮鞯亩际荄TO對(duì)象類型了,我們?cè)诙x的時(shí)候,為了避免更多的改動(dòng),依舊使用Info這樣的類名稱作為DTO對(duì)象的名稱,代表表名對(duì)象。
在混合式框架的界面表現(xiàn)層,它們的數(shù)據(jù)對(duì)象的處理基本上保持和原來的代碼差不多。

/// <summary>
/// 新增狀態(tài)下的數(shù)據(jù)保存
/// </summary>
/// <returns></returns>
public override bool SaveAddNew()
{
    UserInfo info = tempInfo;//必須使用存在的局部變量,因?yàn)椴糠中畔⒖赡鼙桓郊褂?    SetInfo(info);
    info.Creator = Portal.gc.UserInfo.FullName;
    info.Creator_ID = Portal.gc.UserInfo.ID.ToString();
    info.CreateTime = DateTime.Now;

    try
    {
        #region 新增數(shù)據(jù)

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

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

但我們需要在WCF服務(wù)層說明他們之間的映射關(guān)系,方便進(jìn)行內(nèi)部的轉(zhuǎn)換處理。



在實(shí)體框架界面層的查詢中,我們也不在使用部分SQL的條件做法了,采用更加安全的基于DTO的LINQ表達(dá)式進(jìn)行封裝,最后傳遞給后臺(tái)的也就是一個(gè)LINQ對(duì)象(非傳統(tǒng)方式的實(shí)體LINQ,那樣在分布式處理中會(huì)出錯(cuò))。
如查詢條件的封裝處理如下所示:

/// <summary>
/// 根據(jù)查詢條件構(gòu)造查詢語(yǔ)句
/// </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));
    }
.........................................

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

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

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

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

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 = "系統(tǒng)用戶信息報(bào)表";

最后我們來看看整個(gè)實(shí)體框架的結(jié)構(gòu)和界面的效果介紹。
界面效果如下所示:



代碼結(jié)構(gòu)如下所示:



架構(gòu)設(shè)計(jì)的效果圖如下所示:
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容