Dynamic LINQ 讓 LINQ 的世界變的更美好

身為一個程式設計師最討厭的就是被侷限在框框裡寫程式,我們唯一的限制應該是在我們的創意而非架構。像 LINQ 剛出來的時候我覺得超級好用,不過等用在專案上實做的時候才發現綁手綁腳的,想要動態組成一個 LINQ 語法難上加難,結果過沒多久就在 ScottGu's Blog 看到一篇 Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library),看到的時候好像挖到寶一樣,以前在開發時卡住的問題全部都解決了,從今年 2 月份用到現在簡直是幾乎忘了它的存在,以致於這幾個月都沒有提及 Dynamic LINQ 這個好東西,直接上週一個朋友問我:『LINQ 的 Where 條件可以像以前組 SQL Command 一樣動態組裝嗎?』,我馬上跟他說有 Dynamic LINQ 這個東西,不用多說,他跟我剛看到 Dynamic LINQ 的時候一樣覺得:Bravo! ( 太棒了 )

這個 Dynamic LINQ 其實也跟我曾經介紹過的 好用的 Linq Samples and the Sample Query Explorer 一樣,早就出現在 VS2008 的 CSharpSample 裡面了,有需要的人可以到這裡下載,下載回來後這個 Library 放在 \LinqSamples\DynamicQuery\ 專案裡,專案裡面有個 Dynamic.cs 是一堆 Extension Methods,用來擴充 LINQ 的現有功能,程式碼有點複雜,想專精 LINQ 的人可以好好研究一下這些原始碼,共有 2,027 行。如果沒空看也沒關係,會用就很夠了,你可以看專案內的 Program.cs 提供一些簡單的範例,不用多說,看個範例你就知道他在做什麼了:

Northwind db = new Northwind(connString); 
db.Log = Console.Out;

var query =
    db.Customers.Where("City == @0 and Orders.Count >= @1", "London", 10).
    OrderBy("CompanyName").
    Select("New(CompanyName as Name, Phone)");

Console.WriteLine(query);
Console.ReadLine();

我介紹到這裡就好,大家如果有感覺的話,可以到 Dynamic LINQ (Part 1: Using the LINQ Dynamic Query Library)动态LINQ (第一部分:使用LINQ动态查询库) 查看更完整的範例,或者可以用搜尋引擎查詢 Dynamic LINQ 也可以找到一堆資料。

不過,使用 Dynamic LINQ 有個缺點,就是這種寫法就會失去編譯時期偵錯(Compile-time Debugging)的機會,如果你自己組成的 LINQ 語法有問題的話,就必須要到執行時期(Runtime)才有可能找到錯誤了,這時你就必須要做好錯誤處理(Error Handling)才行!

  

此文章由 will 發表於 2008/6/23 下午 05:17:02

永久連結 | 評論 (0) | 此文章的RSSRSS comment feed |

分類: .Net | C# | LINQ

標籤: ,

收藏:

善用 LINQ to SQL 中的 partial class 機制進行資料格式驗證

我們通常會使用 DetailsView 或 FormView 控制項進行資料的新增、編輯、刪除等動作,而通常我們會在裡面放置許多 Validator 控制項以驗證資料格式是否正確,不過當進行較大的專案時,開發人數通常會比較多,而且人員之間的分工也會比較明確,有的人做 SA、有的人做 DBA、有的人做 ASP.NET UI Process 開發...等等,在分層負責的情況下若能夠有效分割工作,不但專案品質會比較高,大家在寫程式的過程中也會比較專注。

就拿驗證輸入資料的部分來討論,SA/SD 在傳遞專案規格給開發人員時,總是難免有東西沒講清楚,而導致某些資料輸入的驗證沒有做好;或者是專案需求不斷變更,而導致可能有些輸入驗證的規則遺漏了更新程式碼,若能從「資料的角度」出來來驗證輸入的資料,而非「操作介面的角度」來驗證資料,應該是比較直覺且有效的方式,而 LINQ to SQL 正好就實做出了這一點。

當我們用 LINQ to SQL Designer 設計完資料物件模型,預設就有定義一些你可以自行實做的 partial method,例如說之前文章提到過的 OnCreated() 方法,可用於資料物件(Entity)產生時給予資料物件的預設值。今天要講的是另一個 OnValidate() 方法,可用於當 LINQ to SQL 在執行 Insert, Update 或 Delete 等動作之前,對資料物件進行資料驗證的動作。

底下是一個簡單的驗證範例:

public partial class 產品資料
{
    partial void OnValidate(System.Data.Linq.ChangeAction action)
    {
        if (action == System.Data.Linq.ChangeAction.Insert 
            || action == System.Data.Linq.ChangeAction.Update)
        {
            if (!this._繳費方式.HasValue)
            {
                throw new Exception("您沒有輸入「繳費方式」無法儲存產品資料");
            }

            if (this.產品類別.名稱 == "電子商務類")
            {
                if (this.產品售價 < 2000)
                {
                    throw new Exception("電子商務類產品不得設定低於 2000 元。");
                }
            }
        }
    }
}

上面這段程式的「白話文」就是,當使用者要新增資料時或更新資料時,會先檢查「繳費方式」有沒有輸入資料,若沒輸入資料則丟出一個例外事件(Exception)。若「產品資料」的「產品類別」定義為「電子商務類」,當產品售價低於 2000 時則丟出一個例外事件(Exception)。

你可以看到我對產品類別的判斷條件跟「資料格式(Data Format)」是無關的,而是跟「商業邏輯(Business Logic)」有關係,在實務上資料格式異動的機會不太大(總是有例外的),但是商業邏輯就很有可能經常改變,將商業邏輯的判斷寫在這裡會比寫在每一支 ASP.NET 頁面程式裡來的清楚明瞭而直覺,不過這並不代表在 ASP.NET 頁面中就不需額外做出判斷,要做好一個完美的使用者介面,需要考量的細節是很多的,況且客戶的需求總是讓你料想不到,只是你可以把 OnValidate() 方法視為從 ASP.NET 輸入資料到 SQL Server 中的最後一道防線。

當然,你也可以把「最後一道防線」設定在資料庫系統中,例如說撰寫觸發程序(Trigger)或預儲程序(Stored Procedure),但這沒有一定的規則,全看你怎麼規劃整個系統開發的架構,而我個人是偏好寫 C# 大於 T-SQL 啦。^_^

  

此文章由 will 發表於 2008/6/17 上午 01:00:08

永久連結 | 評論 (0) | 此文章的RSSRSS comment feed |

分類: .Net | ASP.NET | LINQ

標籤: ,

收藏:

使用 VS2008 開發 ReportViewer 在部署時的注意事項

我最近用 VS2008 開發了一組報表,但在部署到測試機的時後發生了【無法載入檔案或組件 'Microsoft.ReportViewer.WebForms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' 或其相依性的其中之一。 系統找不到指定的檔案。】的錯誤:

'/' 應用程式中發生伺服器錯誤。
--------------------------------------------------------------------------------

組態錯誤 
描述: 處理服務此要求所需的組態檔時發生錯誤。請檢視下列的特定錯誤詳細資訊,並適當修改您的組態檔。 

剖析器錯誤訊息: 無法載入檔案或組件 'Microsoft.ReportViewer.WebForms, Version=9.0.0.0, 
Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' 或其相依性的其中之一。 系統找不到指定的檔案。

image

因為我們在安裝 Visual Studio 2008 時都已經把所有該安裝的元件都安裝好了,所以部署到沒有 Visual Studio 2008 的主機需要額外安裝 ReportViewer 元件才可以使用。

若您是用 VS2008 開發報表的話,安裝檔可到以下網址下載:

若您是用 VS2005 開發報表的話,安裝檔可到以下網址下載:

  

此文章由 will 發表於 2008/6/16 上午 09:47:40

永久連結 | 評論 (0) | 此文章的RSSRSS comment feed |

分類: .Net | ASP.NET | IIS | 系統管理

標籤: , ,

收藏:

ASP.NET 自訂角色的方式(不用實做 Role Provider)

我之前講過一篇文章叫做概略解釋 Forms Authentication 的運作,但若要使用 Forms Authentication 來驗證使用者又要自行指派使用者的角色時那就麻煩了,通常要實做角色提供者(Role Provider) 才行,不過通常很麻煩,我今天介紹一個更簡單、方便的自訂角色方法。

若使用 Forms Authentication 我常用的登入程式碼如下:

/// <summary>
/// 設定要存在 FormsAuthenticationTicket 中的資料,這裡用來儲存角色資訊
/// </summary>
string userData = "";

protected void Login_Button_Click(object sender, EventArgs e)
{
    // 登入時清空所有 Session 資料
    Session.RemoveAll();

    // 登入的密碼(以 SHA1 加密)
    string strPassword = FormsAuthentication.HashPasswordForStoringInConfigFile(txtPassword.Text, "SHA1");

    if (ValidateLogin(txtAccount.Text, strPassword))
    {
        // 將管理者登入的 Cookie 設定成 Session Cookie
        bool isPersistent = false;

        FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
          txtAccount.Text,
          DateTime.Now,
          DateTime.Now.AddMinutes(30),
          isPersistent,
          userData,
          FormsAuthentication.FormsCookiePath);

        string encTicket = FormsAuthentication.Encrypt(ticket);

        // Create the cookie.
        Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));

        Response.Redirect("~/Members/", true);
    }
    else
    {
        Page.ClientScript.RegisterStartupScript(Page.GetType(), "LoginFail", "alert('登入失敗,請重新登入!');", true);
    }
}

/// <summary>
/// 驗證使用者是否登入成功
/// </summary>
/// <param name="strUsername">登入帳號</param>
/// <param name="strPassword">登入密碼</param>
/// <returns></returns>
private bool ValidateLogin(string strUsername, string strPassword)
{
    // 驗證
    
        // 請自行寫 Code 檢查 Username, Password 是否正確
    
    // 授權:設定角色到 userData 
    userData = "gold_member,board_admin";
    
    return true;
}

而這段程式的重點在於我將 userData 儲存在 FormsAuthenticationTicket 中,讓使用者登入資訊包括「角色」的定義,但是如果你要在程式中使用 User.IsInRole 或要在 SiteMap 檔中啟用 securityTrimmingEnabled 的話,還需要在 Global.asax 中新增 Application_AuthenticateRequest 事件,才能讓 ASP.NET 知道該使用者擁有什麼角色,如下:

void Application_AuthenticateRequest(object sender, EventArgs e){
    if (Request.IsAuthenticated) {
        // 先取得該使用者的 FormsIdentity
        FormsIdentity id = (FormsIdentity)User.Identity;
        // 再取出使用者的 FormsAuthenticationTicket
        FormsAuthenticationTicket ticket = id.Ticket;
        // 將儲存在 FormsAuthenticationTicket 中的角色定義取出,並轉成字串陣列
        string[] roles = ticket.UserData.Split(new char[] { ',' });
        // 指派角色到目前這個 HttpContext 的 User 物件去
        Context.User = new GenericPrincipal(Context.User.Identity, roles);
    }
}

這段程式碼我可是研究好久才整理出來的,但我覺得透過這種方式自訂「角色」感覺簡單多了,而且也不需要自己實做 Role Provider 或在資料庫中多建立一個表格,整體的效能也應該不差才是。

P.S. 若要啟用 SiteMap 安全限制的功能必須要在 web.config 中設定該 sitemap 定義的 securityTrimmingEnabled 屬性為 True 才行,如下:

<siteMap defaultProvider="SiteMapProvider" enabled="true">
    <providers>
        <add name="SiteMapProvider" description="設的 SiteMap 定義檔"
             type="System.Web.XmlSiteMapProvider " siteMapFile="~/Web.sitemap" 
             securityTrimmingEnabled="true"/>
    </providers>
</siteMap>
  

此文章由 will 發表於 2008/6/11 下午 08:17:00

永久連結 | 評論 (0) | 此文章的RSSRSS comment feed |

分類: .Net | ASP.NET

標籤: , ,

收藏:

以 Convert.ToString(Eval("ID")) 取代 Eval("ID").ToString()

在 ASP.NET 頁面套版時,我常看到有工程師是這樣套版的:

<a href="<%# Server.UrlPathEncode(Eval("Attachment").ToString()) %>">XXXX</a>

看出問題了嗎?

是的,當 Attachment 這個欄位的內容是 null 的時候,這裡就會發生例外狀況(Exception),所以這樣的寫法一旦養成習慣,那就等著常常被電啦!

我認為比較好的方式應該是改用以下的寫法:

<a href="<%# Server.UrlPathEncode(Convert.ToString(Eval("Attachment"))) %>">XXXX</a>

其實只是換個角度來想而已!原本是把 Eval("Attachment") 當成「主角」來看待,讓這位物件的主角呼叫 ToString() 回應字串;現在改變成「配角」,將 Eval("Attachment") 變成參數傳入 Convert 類別提供的 ToString() 方法,如果傳入的 Eval("Attachment") 是 null 的話,Convert.ToString(Eval("Attachment")) 傳回的值就是 String.Empty (也就是空字串啦),這樣一來就不會發生 Exception 了。

這是一個很小很小的技巧,但是請銘記在心。

2008-06-20 補充:

  • 我最近發現原來使用 Eval("ID", "") 也可以轉成字串喔,簡短多了,供大家參考!
  

此文章由 will 發表於 2008/6/6 下午 10:04:09

永久連結 | 評論 (2) | 此文章的RSSRSS comment feed |

分類: .Net | ASP.NET | C#

標籤:

收藏:

鬼打牆事件之『ASP.NET 無法刪除 Cookie 的問題』

先將我的執行環境說明一下:

  • 我有兩個網站,網域分別為 www1.domain.com 與 www2.domain.com
  • 自己實做單一簽入(Single Sign On, SSO)的機制,並透過 Web Service 進行網站會員登入、登出
  • 兩個網站共用同一組 Cookie 用以儲存 SSO 的 Token,且明訂 Cookie 的 Domain 為 .domain.com

我今天聚焦在「登出」這個簡單的功能就好,實作登出是在簡單不過的機制了,就是把 Cookie 清除掉就好啦!不過要是跟我一樣遇到 ASP.NET 怎樣也清除不掉 Cookie 的情況,那就真的是「鬼打牆」了,而且若要問人家:「請問 Cookie 要怎麼樣才能刪除掉?」這種問題問別人真的會被笑,也不知道怎麼開口。

因為我要共用 Cookie 在兩個網域,所以我的 Cookie Domain 是 .domain.com,所以照理說清除 Cookie 的方法應該是:

Response.Cookies["Token"].Expires = DateTime.Now.AddYears(-1);

或者是

Response.Cookies["Token"].Expires = DateTime.Now.AddYears(-1);
Response.Cookies["Token"].Domain = ".domain.com";

或者是

Response.Cookies["Token"].HttpOnly = true;
Response.Cookies["Token"].Expires = DateTime.Now.AddYears(-1);
Response.Cookies["Token"].Domain = ".domain.com";

反正能試的都試了,而且以我對 HTTP 底層協定的了解,幾乎在 Web 領域沒有什麼問題是無法解決的,但今天遇到 Cookie 清不掉這個問題,真的讓我滿面愁容,程式越寫越氣,用 Fiddler2 看 HTTP 封包看了幾十遍,就是看不出有任何問題,但 Cookie 就是殺不掉。

但是這問題非得要研究出來不可,就因為這個「無法登出」的「小問題」搞了我快 5 個小時才弄清楚所有來龍去脈,所幸問題有被我追根究柢的解決了,以下是要解決此問題的完整解法:

  • 若要清除跨 Domain 的 Cookie 必須清除兩次,例如說使用者在 www1.domain.com 要執行登出動作,必須要先將 Domain 為 www1.domain.com 的 Cookie 給清除掉,在接著將 Domain 為 .domain.com 的這個 Cookie 清除掉。
  • 因為這兩個 Cookie 為「同名」,全部都叫做 Token,所以無法在一個 HTTP Request 中清除掉兩個同名的 Cookie,所以必須要在不同的兩個 HTTP Request 中個別刪除不同 Domain 的 Cookie。

例如說:你必須先連到 Logout.aspx 頁面,在此頁面先將第一組 Cookie 清除:

HttpCookie cookie = new HttpCookie("Token", ""); 

cookie.HttpOnly = true;
cookie.Expires = DateTime.Now.AddYears(-1); 
cookie.Domain = Request.Url.Host;
Response.SetCookie(cookie);

Response.Redirect("Logout2.aspx", true);

然後再轉址到 Logout2.aspx 將 Parent Domain 的 Cookie 給清除掉:

HttpCookie cookie = new HttpCookie("Token", ""); 

cookie.HttpOnly = true;
cookie.Expires = DateTime.Now.AddYears(-1); 
cookie.Domain = ".domain.com";
Response.SetCookie(cookie);

Response.Redirect("index.aspx", true);

這樣就可以徹底將 Cookie 給清乾淨了,這真是難得的經驗,從沒想到有這種解法,不知道網路上有沒有其他人遇過跟我同樣的問題?

而我的登出程式最後是改成以下這段 Code,濃縮再一支程式裡:

protected void Page_Init(object sender, EventArgs e)
{
    Response.Cache.SetCacheability(HttpCacheability.NoCache); 

    HttpCookie cookie = new HttpCookie("Token", ""); 

    cookie.HttpOnly = true;
    cookie.Expires = DateTime.Now.AddYears(-1); 

    if (Request.QueryString["domain"] == null)
    {
        cookie.Domain = Request.Url.Host;
        Response.SetCookie(cookie);
        Response.Redirect("Logout.aspx?domain=1"), true);
    }
    else
    {
        cookie.Domain = ".domain.com";
        Response.SetCookie(cookie);
        Response.Redirect("index.aspx", true);
    } 
}
  

此文章由 will 發表於 2008/6/4 下午 08:44:02

永久連結 | 評論 (5) | 此文章的RSSRSS comment feed |

分類: .Net | ASP.NET | C# | Web | 心得分享

標籤: , , ,

收藏:

解決 LINQ to SQL 針對更新大型物件的效率極差的問題

我有一個頁面是使用 DetailsView 進行資料的新增與更新動作,其中有個圖片欄位在資料庫中的資料格式為 VarBinary(MAX),直接用來儲存圖檔的二進位內容,在新增資料的時候上傳檔案並存入資料庫中,在更新的時候直更新該欄位的內容,不過在更新的時候執行的時間都非常久,即便是上傳 10KB 左右的圖檔,在執行到 db.SubmitChanges(); 時都會執行個 30 秒以上,如果上傳幾 MB 的檔案就要執行好幾分鐘,所以常常會發生 Timeout 的情況。

這個問題大約困擾我將近一個月的問題,因為實在已經嘗試了太多作法(都是用 LINQ to SQL 的方式)都無法改善效能問題,直到今天用 MSN 問一個朋友這個狀況的時候,問題才剛問完自己就突然靈機一動想到了一個解決方法,也就是嘗試將該 VarBinary(MAX) 欄位設定 UpdateCheck="Never" 看看,結果就真的完全解決效能問題了。

有時候真的很奇怪,自己想不通的問題在問別人時通常會將問題重新思考過一遍,進而更了解問題本身。所以人家才會常說:「當你會問題的時候,問題就已經先解決一半」,我非常的感同身受。

首先,先開啟你的 *.dbml 檔,找出這個欄位:

<Column Name="ContentData" Type="System.Data.Linq.Binary" DbType="VarBinary(MAX) NOT NULL" 
        CanBeNull="false" />

然後將該欄位加上 UpdateCheck="Never" 屬性,然後儲存。

<Column Name="ContentData" Type="System.Data.Linq.Binary" DbType="VarBinary(MAX) NOT NULL" 
        CanBeNull="false" UpdateCheck="Never" />

加上這個屬性之後,LINQ to SQL 對這個表格就會採用「閉式並行存取控制(Pessimistic Concurrency Control)」的方式進行更新作業,也就是說當 LINQ to SQL 在更新 Binary 格式的欄位時就不會在去判斷檔案的內容是否有異動,直接將更新的檔案內容直些寫入資料庫。

相關連結

  

此文章由 will 發表於 2008/6/2 下午 02:03:45

永久連結 | 評論 (1) | 此文章的RSSRSS comment feed |

分類: .Net | ASP.NET | C# | LINQ

標籤: , ,

收藏:

推薦:Microsoft Visual Studio International Pack 1.0 版

Microsoft Visual Studio International Pack 是一套國產的類別庫喔,是由台灣微軟的工程師所開發的,主要是幫助 .NET 程式開發人員建立全球化的應用程式,其中包括許多功能,其中有一套我們很可能會用到的「繁簡轉換」功能,也就是「中文繁簡轉換類別庫」。

在你下載回 vsintlpack1.zip 檔案並解壓縮後,會有個 CHTCHSConv.msi 安裝檔,這個就是「中文繁簡轉換類別庫及 Add-In 工具」,安裝好之後相關檔案會放在以下目錄(好長的目錄名稱啊):

C:\Program Files\Microsoft Visual Studio International Pack\Traditional Chinese to Simplified Chinese Conversion Library and Add-In Tool

你只要將該目錄中的 ChineseConverter.dll 組件複製到你網站的 Bin 目錄下就能用了,要使用之前必須先引用其命名空間:

using Microsoft.International.Converters.TraditionalChineseToSimplifiedConverter;

其中只有一個 ChineseConverter 靜態類別與一個 ChineseConversionDirection 列舉(enum),使用方式很簡單,如果要將繁體字轉成簡體字可參考以下程式碼:

string TWS = "領導有策略";
string CHS = ChineseConverter.Convert(TWS, ChineseConversionDirection.TraditionalToSimplified);

如果要將簡體字轉成繁體字可參考以下程式碼:

string CHS = "领导有策略";
string TWS = ChineseConverter.Convert(CHS, ChineseConversionDirection.SimplifiedToTraditional);

夠簡單吧!

除了單純的「字對字」轉譯之外,如果使用者電腦有安裝 Microsoft Office 2007 的話(使用這個組件的主機),他還會利用 Office 2007 內建的一些「詞彙轉譯」功能,發現雖然文件說有安裝 Microsoft Office 2007 可以讓中文繁簡體轉換出來的品質更好,不過我測試了一下發現其實沒差,不知道品質好在哪裡?不過不失為一個好用的工具啦! ^_^

  

此文章由 will 發表於 2008/5/29 下午 08:20:24

永久連結 | 評論 (0) | 此文章的RSSRSS comment feed |

分類: .Net | C# | Office | Visual Studio

標籤: , , ,

收藏:

C# 3.0 初始設定 Hashtable 的方式

C# 3.0 有個特性叫做「物件和集合初始設定式」(Object and Collection Initializers),一般的用法是:

Customer c1 = new Customer
    {
        Name = "Will Huang", 
        Age = 30
    };

不過今天在寫 Code 的時候因為寫到 Hashtable 時卡住了,突然忘記要怎麼寫 Hashtable 的初始值的方式,上網查了一下寫法,原來很簡單,用大括弧框起每一筆即可。

Hashtable GenderMapping = new Hashtable()
    {
        {"男", true},
        {"女", false}
    }; 

相關連結

  

此文章由 will 發表於 2008/5/27 下午 11:59:01

永久連結 | 評論 (0) | 此文章的RSSRSS comment feed |

分類: .Net | C#

標籤: , ,

收藏:

解決 LINQ to SQL 資料庫更新衝突的情形

我前陣子遇到一個偶發的錯誤狀況,就是我在我某個頁面中需要計算文件下載的次數,因此需要每次進入頁面時都要讓該筆資料的 num 欄位的值自動加 1,也就是每次都要更新資料庫,但是每過幾天就有可能收到幾個 System.Data.Linq.ChangeConflictException 例外狀況,錯誤訊息如下:

中文版

System.Data.Linq.ChangeConflictException: 資料列找不到,或者已變更。
英文版
System.Data.Linq.ChangeConflictException: Row not found or changed.

這原因就出在當我用 LINQ to SQL 將資料取出之後,一直到寫回資料庫的過程中,資料庫中的該筆資料發生了變更,而導致衝突狀況,我的程式碼如下:

MyTable m = (from p in db.MyTable
             where p.ID.CompareTo(id) == 0
             select p).FirstOrDefault();
if (m != null)
{
    m.num = m.num + 1;
    db.SubmitChanges();
}

雖然這段程式從取出之後就立即將更新的值送回資料庫更新,不過當網站流量大的時候這種資料更新的衝突現象似乎無法避免,我這幾天研究出 3 種可能的解決方案:

第一種:直接對資料庫下 SQL 指令(不使用 LINQ 的標準更新方式)

db.ExecuteCommand("UPDATE [dbo].[MyTable] SET num=num+1 WHERE ID = @p0", m.ID);

這應該是最簡單直覺的作法了,也不會有衝突的狀況發生,如果你只是要做簡單的「計數器」功能,建議用這一招就好了,否則請看第二種方法。

第二種:使用 LINQ to SQL 變更衝突的處理方法

在 MSDN 的 HOW TO:管理變更衝突 (LINQ to SQL) 文章有列出一些關於此主題的說明,建議要寫 LINQ to SQL 的開發人員務必熟讀此章節。

除了以上這些文章外,應該也要看看 開放式並行存取概觀 (LINQ to SQL),如果覺得中文看不懂也可以看看英文版的 Optimistic Concurrency Overview (LINQ to SQL),因為我在看文章時有些翻譯說實在還看不太習慣。

底下是解決衝突問題的範例程式(參考  ObjectChangeConflict.Resolve 方法 (RefreshMode) 說明)

try
{
    db.SubmitChanges(System.Data.Linq.ConflictMode.ContinueOnConflict);
}
catch (System.Data.Linq.ChangeConflictException ex)
{
    foreach (System.Data.Linq.ObjectChangeConflict occ in db.ChangeConflicts)
    {
        // *********************************************
        // 底下三個範例是 3 選 1 喔,不要三行都寫在一起!
        // **********************************************

        // 採用資料庫的查詢出來的值,目前物件的值將會被資料庫最新查到的複寫
        occ.Resolve(System.Data.Linq.RefreshMode.OverwriteCurrentValues);
        
        // 採用目前物件中的值,並更新資料庫中的版本
        occ.Resolve(System.Data.Linq.RefreshMode.KeepCurrentValues);
        
        // 僅更新此物件中變更的欄位,僅將變更的欄位寫入資料庫(或稱為合併更新)
        occ.Resolve(System.Data.Linq.RefreshMode.KeepChanges);
    }

    // 注意:解決完衝突之後要記得重新再 SubmitChanges() 一次,否則一樣不會更新資料庫
    db.SubmitChanges();
}

我在驗證變更衝突的測試程式的完整原始碼如下:

db = new NEXCOMDataContext();

MyTable m = (from p in db.MyTable
             where p.ID.CompareTo(MyTableID) == 0
             select p).FirstOrDefault();

if (m != null)
{
    // 刻意引發變更衝突
    db.ExecuteCommand(@"UPDATE [dbo].[MyTable] SET num = num - 1 WHERE ID={0}", MyTableID);

    m.num = m.num + 1;

    try
    {
        db.SubmitChanges(System.Data.Linq.ConflictMode.ContinueOnConflict);
    }
    catch (System.Data.Linq.ChangeConflictException ex)
    {
        Response.Write(String.Format("<xmp>ChangeConflictException = {0}</xmp>", ex.Message));
        
        foreach (System.Data.Linq.ObjectChangeConflict occ in db.ChangeConflicts)
        {
            // 採用目前物件中的值,並更新資料庫中的版本
            //occ.Resolve(System.Data.Linq.RefreshMode.KeepCurrentValues);

            // 採用資料庫的查詢出來的值,目前物件的值將會被資料庫最新查到的複寫
            //occ.Resolve(System.Data.Linq.RefreshMode.OverwriteCurrentValues);
           
            // 僅更新此物件中變更的欄位,僅將變更的欄位寫入資料庫(合併)
            occ.Resolve(System.Data.Linq.RefreshMode.KeepChanges);
        }
        // 注意:解決完衝突之後要記得重新再 SubmitChanges() 一次,否則一樣不會更新資料庫
        db.SubmitChanges();
    }
}

Response.End();

變更衝突是開發資料庫應用經常會發生的問題,觀念務必要清楚明瞭,下次遇到問題的時候才能快速反應出最正確的解決方案。

第三種:採用封「閉式並行存取控制(Pessimistic Concurrency Control)」,或也有人稱為「悲觀同步存取控制」

只要在 LINQ to SQL Designer 中將特定的欄位的 UpdateCheck 屬性設定為 Never,就可以避免在更新資料時發生變更衝突。只不過當衝突發生的時候,資料庫中新的值可能會被目前物件的值給蓋過去,數字會有點不精確就是了。

    在 LINQ to SQL Designer 中將特定的欄位的 UpdateCheck 屬性設定為 Never

相關連結

  

此文章由 will 發表於 2008/5/24 下午 06:52:05

永久連結 | 評論 (0) | 此文章的RSSRSS comment feed |

分類: .Net | ASP.NET | C# | LINQ

標籤: , ,

收藏: