這陣子的專案幾乎每個都會用到 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 點是我最近發現的小狀況,如果日後有發現新的狀況我還會補充上來。