The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

在撰寫 LINQ to SQL 時應注意的幾個小地方

這陣子的專案幾乎每個都會用到 LINQ to SQL 技術,但我發現有些人在撰寫程式碼的時候有些不太好的習慣,會對資料庫進行一些多餘的查詢動作或建立多餘的 DataContext,以下是我最近觀察到的幾種狀況與建議的寫法:

1. 一個頁面中只需要共用一個 DataContext

錯誤的程式碼

var q = from p in SQLHelper.GetDataContext().News
        select p;

建議的程式碼

MyDataContext db = SQLHelper.GetDataContext();

var q = from p in db.News
        select p;

說明:所有 LINQ to SQL 語法都共用 db 變數即可,不需要每次執行 LINQ to SQL 都產生一個新的 DataContext 浪費資源。

2. 取出結果的第一筆資料不要重複執行 q.First() 方法

錯誤的程式碼

var q = from p in db.News
        where p.ID.CompareTo(new Guid(Request.QueryString["id"])) == 0
        select p;
if(q.First().Title.Contains(strKeyword) || 
   q.First().Content.Contains(strKeyword)) {
    return true;
}

建議的程式碼

var q = from p in db.News
        where p.ID.CompareTo(new Guid(Request.QueryString["id"])) == 0
        select p;
News n = q.First();
if(n.Title.Contains(strKeyword) || n.Content.Contains(strKeyword)) {
    return true;
}

說明:你每次執行 q.First() 他都會進資料庫做一次資料查詢,你判斷五次就會執行五次,是很沒效率的作法。直接呼叫 q.First() 也有風險,請看第 3 點的說明。

3. 若要取得單筆資料,要判斷是否有從資料庫中取到資料時不要用 q.Count() 方法

錯誤的程式碼

var q = from p in db.News
        where p.ID.CompareTo(new Guid(Request.QueryString["id"])) == 0
        select p;

if (q.Count() == 1)
{
    m = q.First();
}
else
{   
    Response.Redirect("/", true);
}

建議的程式碼

var q = from p in db.News
        where p.ID.CompareTo(new Guid(Request.QueryString["id"])) == 0
        select p;

News n = q.FirstOrDefault(); // 如果用 q.First() 在沒資料時會發生 Exception

if (n == null)
{
    Response.Redirect("/", true);
    return;
}

說明:你每執行一次 q.Count() 方法,程式都會進資料庫執行一遍 SELECT COUNT(*) 的動作,如果你只需要取出一筆資料的話,這個動作其實是多餘的。如果你用的是 q.First() 來取得第一筆資料的話,當資料庫沒資料時是會發生 Exception 的!

4. 不要在 LINQ 語法中轉型(Casting)或執行太多的 .NET 方法

錯誤的程式碼

var q = from p in db.News
        where p.ID.CompareTo(new Guid(Request.QueryString["id"])) == 0
        select p;

建議的程式碼

try {
    Guid id = new Guid(Request.QueryString["id"]);
} catch {
    Response.Redirect("/", true);
    return;
}
var q = from p in db.News
        where p.ID.CompareTo(id) == 0
        select p;

說明:以本範例為例,如果你傳入的 id 不是有效的 Guid 字串,就會發生例外事件。這個例外事件會在 LINQ to SQL 在執行的過程中發生失敗 (Inner Exception),他會給你類似這樣的錯誤訊息:

System.Web.HttpUnhandledException: Exception of type 'System.Web.HttpUnhandledException' was thrown. ---> System.ArgumentNullException: Value cannot be null.
Parameter name: g
   at System.Data.Linq.SqlClient.QueryConverter.VisitInvocation(InvocationExpression invoke)
   at System.Data.Linq.SqlClient.QueryConverter.VisitInner(Expression node)
   at System.Data.Linq.SqlClient.QueryConverter.VisitMethodCall(MethodCallExpression mc)
   at System.Data.Linq.SqlClient.QueryConverter.VisitInner(Expression node)
   at System.Data.Linq.SqlClient.QueryConverter.VisitExpression(Expression exp)
   at System.Data.Linq.SqlClient.QueryConverter.VisitBinary(BinaryExpression b)
   at System.Data.Linq.SqlClient.QueryConverter.VisitInner(Expression node)
   at System.Data.Linq.SqlClient.QueryConverter.VisitExpression(Expression exp)
   at System.Data.Linq.SqlClient.QueryConverter.VisitBinary(BinaryExpression b)
   at System.Data.Linq.SqlClient.QueryConverter.VisitInner(Expression node)
   at System.Data.Linq.SqlClient.QueryConverter.VisitExpression(Expression exp)
   at System.Data.Linq.SqlClient.QueryConverter.VisitWhere(Expression sequence, LambdaExpression predicate)
   at System.Data.Linq.SqlClient.QueryConverter.VisitSequenceOperatorCall(MethodCallExpression mc)
   at System.Data.Linq.SqlClient.QueryConverter.VisitInner(Expression node)
   at System.Data.Linq.SqlClient.QueryConverter.VisitAggregate(Expression sequence, LambdaExpression lambda, SqlNodeType aggType, Type returnType)
   at System.Data.Linq.SqlClient.QueryConverter.VisitSequenceOperatorCall(MethodCallExpression mc)
   at System.Data.Linq.SqlClient.QueryConverter.VisitInner(Expression node)
   at System.Data.Linq.SqlClient.QueryConverter.ConvertOuter(Expression node)
   at System.Data.Linq.SqlClient.SqlProvider.BuildQuery(Expression query, SqlNodeAnnotations annotations)
   at System.Data.Linq.SqlClient.SqlProvider.System.Data.Linq.Provider.IProvider.Execute(Expression query)
   at System.Data.Linq.DataQuery`1.System.Linq.IQueryProvider.Execute[S](Expression expression)
   at System.Linq.Queryable.Count[TSource](IQueryable`1 source)
   at ASP.masterpage_master.Page_Load(Object sender, EventArgs e) in c:\XXX\AAA\BBB\MasterPage.master:line 20
   at System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o, Object t, EventArgs e)
   at System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e)
   at System.Web.UI.Control.OnLoad(EventArgs e)
   at System.Web.UI.Control.LoadRecursive()
   at System.Web.UI.Control.LoadRecursive()
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   --- End of inner exception stack trace ---
   at System.Web.UI.Page.HandleError(Exception e)
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest()
   at System.Web.UI.Page.ProcessRequest(HttpContext context)
   at ASP.productmodel_aspx.ProcessRequest(HttpContext context) in c:\WINDOWS\Microsoft.NET\Framework64\v2.0.50727\Temporary ASP.NET Files\root\52723026\465b3099\App_Web_xglot_f2.3.cs:line 0
   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

這樣的錯誤訊息將會讓你比較不容易除錯。

5. 使用具型別的語法時發生無法 Compile 的情形

例如說你有個 LINQ 語法如下:

var q = from p in db.News
        select new { p.ID, p.Title, p.Summary };

var myList = q.ToList();

上面這斷的 LINQ 語法選出來的資料在執行 ToList() 方法之後變成了一組「匿名型別(Anonymous Type)集合」,但是匿名型別是無法當成參數傳遞的,也無法序列化(Serialization),也就是說你沒辦法把這些資料存在 ViewState、Session 或 Cache 物件裡!

通常這種情況我們會自訂一個類別 ,用來儲存透過 LINQ to SQL 所 select 出來的欄位,例如說以下程式:

public class MyNews
{
    public Guid ID;
    public string Title;
    public string Summary;
}

而你原本的 LINQ 語法要改成這樣:

var q = from p in db.News
        select new MyNews { p.ID, p.Title, p.Summary };

如果你這樣寫的話,那你就錯了!因為這樣的程式碼在編譯的時候會出現以下錯誤訊息:

Cannot initialize type 'MyNews' with a collection initializer because it does not implement 'System.Collections.IEnumerable'

當你看著這個錯誤訊息,你可能會想趕快在 MyNews 類別上實做 System.Collections.IEnumerable 介面,但是問題根本不在這裡!

你必須「明確指定」該類別的欄位名稱(Field Name)才可以正常編譯,如下程式範例:

var q = from p in db.News
        select new MyNews { ID=p.ID, Title=p.Title, Summary=p.Summary };

有了明確的型別,我們透過 ToList() 方法取得的資料就可以當成參數傳遞了,也可以將取得的資料 Cache 起來,雖然是很小的地方,但第一次遇到的人可能會弄很久才解決!

以上這 5 點是我最近發現的小狀況,如果日後有發現新的狀況我還會補充上來。