﻿# 在撰寫 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](http://en.wikipedia.org/wiki/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 點是我最近發現的小狀況，如果日後有發現新的狀況我還會補充上來。
