一般而言,我們使用LINQ to SQL更新資料時,程序為:

  1. 建立DataContext
  2. 透過from o in ... where ... select o 取出某筆資料物件(例如: m)
  3. 設定新值,例如: m.Property = newValue
  4. DataContext.SubmitChanges()
  5. 大功告成!

這裡有個假設前題是,全程中DataContext一直存在,以便掌握所有透過它取出的資料物件被更改的狀況。但有個情境是: 如果我將查詢到的資料物件傳遞到DataContext管不到的範圍,例如: 透過Web Service呼叫變更內容、或轉成JSON字串送到網頁端修改,等修改後物件傳回時,原本用來取出資料的DataContext已不復存在,無從SubmitChanges(),那麼應該如何完成該筆資料的更新呢?

用個實例來說明比較明確些。假設我們有一個ASP.NET網頁以JSON方式進行資料更新(LINQ to SQL資料模型就借用之前範例中的Member),整個操作步驟為:

  1. Client-Side以$.getJSON()呼叫Default.aspx
  2. Server-Side建立DataContext,用LINQ取得Member物件,以JavaScriptSerializer傳換成JSON傳回。基於ASP.NET Page的運作模型,DataContext會在傳回HTTP Response後消失。
  3. Client-Side將JSON字串還原回Javascript Object,更改UserName
  4. 將改過UserName的Member物作以JSON.stringify轉成JSON字串,以$.post傳回Default.aspx?m=update
  5. 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(); }

Post a comment