CODE-LINQ to SQL兩段式更新
6 |
一般而言,我們使用LINQ to SQL更新資料時,程序為:
- 建立DataContext
- 透過from o in ... where ... select o 取出某筆資料物件(例如: m)
- 設定新值,例如: m.Property = newValue
- DataContext.SubmitChanges()
- 大功告成!
這裡有個假設前題是,全程中DataContext一直存在,以便掌握所有透過它取出的資料物件被更改的狀況。但有個情境是: 如果我將查詢到的資料物件傳遞到DataContext管不到的範圍,例如: 透過Web Service呼叫變更內容、或轉成JSON字串送到網頁端修改,等修改後物件傳回時,原本用來取出資料的DataContext已不復存在,無從SubmitChanges(),那麼應該如何完成該筆資料的更新呢?
用個實例來說明比較明確些。假設我們有一個ASP.NET網頁以JSON方式進行資料更新(LINQ to SQL資料模型就借用之前範例中的Member),整個操作步驟為:
- Client-Side以$.getJSON()呼叫Default.aspx
- Server-Side建立DataContext,用LINQ取得Member物件,以JavaScriptSerializer傳換成JSON傳回。基於ASP.NET Page的運作模型,DataContext會在傳回HTTP Response後消失。
- Client-Side將JSON字串還原回Javascript Object,更改UserName
- 將改過UserName的Member物作以JSON.stringify轉成JSON字串,以$.post傳回Default.aspx?m=update
- Server-Side再以JavaScriptSerializer將JSON字串轉換回Member物件,但剛才取出它的DataContext已經不見了,要怎麼SubmitChanges()?
探究LINQ to SQL更新的運作原理,Member物件在各屬性上掛了事件,一旦值被修改,就會偷偷做記號,等SubmitChanges()時,才知道有多少地方要更新。掌握了這個原理,如果我們重新建立另一個DataContext,選取同一個物件,再逐一將有變化的值同步到該物件上,再用新建DataContext.SubmitChages()就可完成我們期望的資料更新動作了。
[2010-06-13]感謝Kenny提醒,LINQ to SQL已有Attach()可以處理資料個體在不同DataContext間移動時的更新或刪除需求(當年Study時肯定是恍神了,這是上進中年人程設職涯最大的絆腳石啊)。換句話說,以下程式碼是恍神中年人另外又發明的輪子,請大家忽略之。XD
[2010-06-25補充]關於Attach()應用範例,可參考相關文。
因此我想到可以做一個萬用Method ApplyChange(object target, object changed),透過Reflection的技巧,假設target與changed是同型別物件,則此方法可將changed上的新值更新到target上,這樣就可以讓新的DataContext感應到哪些屬性有變動需要更新,順利SubmitChanges()。
以下程式範例實現了前述構想,在此列出供大家參考:
<%@ Page Language="C#" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Linq" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Reflection" %>
<%@ Import Namespace="System.Data.Linq.Mapping" %>
<%@ Import Namespace="System.Data.Linq" %>
<%@ Import Namespace="System.Web.Script.Serialization" %>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
JavaScriptSerializer jss = new JavaScriptSerializer();
switch (Request["m"])
{
case "read":
using (PlaygroundDataContext db = new PlaygroundDataContext())
{
//使用LINQ取出一筆資料物件
Member m = (from o in db.Members
where o.UserId == 1
select o).Single();
Response.Write(jss.Serialize(m));
Response.End();
}
break;
case "update":
//警告: 此處應加上CSRF防護,本範例未列出
//參考: http://bit.ly/cu0uKb, http://bit.ly/c18dsN
StreamReader sr = new StreamReader(Request.InputStream);
Member changed = jss.Deserialize<Member>(sr.ReadToEnd());
using (PlaygroundDataContext db = new PlaygroundDataContext())
{
//使用LINQ取出一筆資料物件
Member m = (from o in db.Members
where o.UserId == 1
select o).Single();
ApplyChange(m, changed);
Response.ContentType = "text/plain";
db.Log = Response.Output;
db.SubmitChanges();
Response.End();
}
break;
}
}
/// <summary>
/// 找出LINQ資料物件的Primary Key欄位名稱
/// </summary>
/// <param name="obj">LINQ資料物件</param>
/// <returns>PK欄位名稱字串陣列</returns>
public static string[] FindPrimaryKeyColNames(object obj)
{
Type t = obj.GetType();
List<string> pk = new List<string>();
foreach (PropertyInfo pi in t.GetProperties())
{
object[] atts = pi.GetCustomAttributes(true);
foreach (object att in atts)
{
ColumnAttribute colAtt = att as ColumnAttribute;
if (
att is ColumnAttribute &&
(colAtt as ColumnAttribute).IsPrimaryKey
)
pk.Add(pi.Name);
}
}
return pk.ToArray();
}
/// <summary>
/// 將物件修改值併入另一個同型別物件
/// </summary>
/// <param name="target">欲套用新值的物件</param>
/// <param name="change">內含新值的物件</param>
public static void ApplyChange(object target, object change)
{
Type t1 = target.GetType();
Type t2 = target.GetType();
//檢查二者的型別必須相同
if (t1 != t2)
throw new ApplicationException("Type dismatch!");
//找出Primary Key
string[] pk = FindPrimaryKeyColNames(target);
if (pk.Length == 0)
throw new ApplicationException("No primary key found!");
Dictionary<string, PropertyInfo> props =
new Dictionary<string, PropertyInfo>();
foreach (PropertyInfo pi in t1.GetProperties())
props.Add(pi.Name, pi);
//比對二者的PK是否相同
foreach (string c in pk)
{
PropertyInfo pi = props[c];
object v1 = pi.GetValue(target, null);
object v2 = pi.GetValue(change, null);
if (!v1.Equals(v2))
throw new ApplicationException("Primary key dismatch!");
}
//比對數值,若不相同,則進行更換,但不包含PK
foreach (PropertyInfo pi in props.Values)
{
if (pk.Contains(pi.Name)) continue;
object v1 = pi.GetValue(target, null);
object v2 = pi.GetValue(change, null);
if (!v1.Equals(v2))
pi.SetValue(target, v2, null);
}
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Two Phase LINQ to SQL Update</title>
<script src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.2.js?WT.mc_id=DOP-MVP-37580"
type="text/javascript"></script>
<script src="json4ms.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
$("#btnTest").click(function () {
$.getJSON("default.aspx", { m: "read", rnd: Math.random() },
function (m) {
m.RegTime = JSON.dateStringToDate(m.RegTime);
alert(m.UserName);
m.UserName = "X" + new Date().getMilliseconds();
$.post("default.aspx?m=update", JSON.stringify(m),
function (r) {
alert(r);
});
});
});
});
</script>
</head>
<body>
<input type="button" id="btnTest" value="Test" />
</body>
</html>
Comments
# by Kenny
在Table(Of TEntity)有一個Attach 方法,透過這個方法可以提供Entity Object在不同的DataContext中能被附加,以便在不同的DataContext中進行更新等動作,這個部份在黃忠成老師的「極意之道 次世代.Net Framework 3.5 資料庫開發聖典」中有提到,從4-69到4-71頁,另外MSDN的說明如下:http://msdn.microsoft.com/zh-tw/library/bb548978(v=VS.90).aspx 不過這個部份有幾個問題: 1.使用Attach的方法在沒有關聯的Entity Object上必須達成以下兩個條件之一: (1)資料表內有timestamp的欄位。 (2)需要將Entity Object所有欄位的「更新檢查」設為「永不」。 2.使用在具有關聯的Entity Object的情況下,就必須額外的處理,處理之後就回到1的兩個條件選一的情況。所謂的額外處理可以參考這篇:http://www.ok3w.net/article/7965.html 另外MSDN也有一篇「N-Tier 應用程式中的資料擷取和 CUD 作業 (LINQ to SQL)」的範例,不過這個範例似乎是在沒有關聯的情況下:http://msdn.microsoft.com/zh-tw/library/bb546187(v=VS.90).aspx 以上是我這兩天剛好在思考相關的問題,透過RSS訂閱看到黑暗大這兩天發表的這篇應該和這個有關,所以自己試一試之後的心得,請指教。:)
# by Jeffrey
to Kenny, 謝謝你的完整補充!! 我遺漏了Attach(),自己磨了一顆輪子 XD,已將Attach說明加入文中...
# by charis@datadoctor.biz
Well I want to know the objects of retrieved from LINQ to SQL, and what is the story for cross-database CRUD operations in LINQ?
# by charis@datadoctor.biz
Well I want to know the objects of retrieved from LINQ to SQL, and what is the story for cross-database CRUD operations in LINQ?
# by Jeffrey
to charis, IMHO, Since we can use reflection trick to copy values to object from different datacontext, it's possible to use it on cross-database create, and update. I think maybe linked server is also a good way to solve cross-database issue. ref: http://bit.ly/9nNaLW
# by hectorlee369
我試著用黑大的sample, 測試發現當有ForeignKey時, 因為change object並未將FK Object一併放入, 導致FK的Value被reset成初始值. 故仿照您的code將FK排除 public static string[] FindForeignKeyColNames(object obj) { Type t = obj.GetType(); List<string> fk = new List<string>(); foreach (PropertyInfo pi in t.GetProperties()) { object[] atts = pi.GetCustomAttributes(true); foreach (object att in atts) { AssociationAttribute colAtt = att as AssociationAttribute; if ( att is AssociationAttribute && (colAtt as AssociationAttribute).IsForeignKey ) fk.Add(pi.Name); } } return fk.ToArray(); }