CUD 代表 Create(新增)、Update(更新)、Delete(刪除)三種基本資料操作。雖然這些主題已被廣泛討論,但 LINQ to SQL 仍有一些獨特的實作細節值得深入探討。
LINQ to SQL 提供 CreateDatabase() 方法,讓開發者能夠透過物件關聯設計工具(OR Designer)定義實體類別與欄位對應,再直接建立資料庫結構。這完美支援「先有領域模型,後有資料庫結構」的自上而下開發模式。
Northwind db = new Northwind("YourConnectionString"); // 資料庫名稱設為不存在的名稱
db.Log = Console.Out;
if (!db.DatabaseExists())
db.CreateDatabase(); // 自動建立資料庫與資料表
雖然 OR Designer 視覺化操作方便,但在複雜資料庫屬性設定上較為繁瑣,實務上常採用「先設計資料表,再反向產生實體類別」的工作流程。
建立實體物件,加入對應的 Table<T> 集合,呼叫 SubmitChanges() 即可完成插入:
var newCustomer = new Customer {
CustomerID = "MCSFT",
CompanyName = "Microsoft",
ContactName = "John Doe",
ContactTitle = "Sales Manager",
Address = "1 Microsoft Way",
City = "Redmond",
Region = "WA",
PostalCode = "98052",
Country = "USA",
Phone = "(425) 555-1234",
Fax = null
};
db.Customers.Add(newCustomer);
db.SubmitChanges();
以 Orders 表的 OrderID 為例,其對應屬性標記為:
[Column(Storage="_OrderID", AutoSync=AutoSync.OnInsert,
DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)]
public int OrderID { get; set; }
AutoSync=AutoSync.OnInsert 指示執行期在插入後自動取回資料庫產生的值(如 SCOPE_IDENTITY())並回填至物件。實測程式碼:
Orders o = new Orders();
o.ShipAddress = "Test";
db.Orders.Add(o);
db.SubmitChanges();
Console.WriteLine(o.OrderID); // 此處已可取得資料庫產生的 OrderID
產生的 SQL 顯示 LINQ to SQL 自動包裝交易,先執行 INSERT,緊接著查詢 SCOPE_IDENTITY() 確保取回正確值:
INSERT INTO [dbo].[Orders]([CustomerID], [EmployeeID], ...) VALUES (@p0, @p1, ...)
SELECT [t0].[OrderID] FROM [dbo].[Orders] AS [t0] WHERE [t0].[OrderID] = (SCOPE_IDENTITY())
當新增「一」端實體(如 Category)時,若其導覽屬性集合(Products)已關聯新實體,呼叫 SubmitChanges() 會自動級聯插入「多」端資料:
var newCategory = new Category { CategoryName = "Widgets", Description = "..." };
var newProduct = new Product { ProductName = "Blue Widget", UnitPrice = 34.56M, Category = newCategory };
db.Categories.Add(newCategory);
db.SubmitChanges(); // 同時插入 Category 與 Product
多對多關聯(透過中介表)需手動建立三端實體並分別加入對應集合:
var newEmployee = new Employee { FirstName = "Kira", LastName = "Smith" };
var newTerritory = new Territory { TerritoryID = "12345", TerritoryDescription = "Anytown", Region = db.Regions.First() };
var newEmployeeTerritory = new EmployeeTerritory { Employee = newEmployee, Territory = newTerritory };
db.Employees.Add(newEmployee);
db.Territories.Add(newTerritory);
db.EmployeeTerritories.Add(newEmployeeTerritory);
db.SubmitChanges();
同一 DataContext 內更新極為簡單:查詢物件、修改屬性、呼叫 SubmitChanges()。跨 DataContext 更新需使用 Attach() 方法,並建議在主鍵屬性加上 IsVersion=true 以利樂觀並發檢查:
[Column(Storage="_PageID", ..., IsVersion=true)]
public int PageID { get; set; }
關鍵原則:更新哪個實體,就 Attach 哪個實體。 錯誤範例:修改 Order.Customer.City 卻 Attach(Order),會導致錯誤插入新客戶;正確做法是 Attach(o.Customer, true)。
// 第一個 Context 取得資料
nwind.Order o = null;
using (nwind.Northwind db = new nwind.Northwind(constr)) {
o = db.Orders.First();
o.Customer.City = "new city";
}
// 第二個 Context 更新
using (nwind.Northwind db = new nwind.Northwind(constr)) {
db.Log = Console.Out;
// db.Orders.Attach(o, true); // 錯誤:會嘗試插入新 Customer
db.Customers.Attach(o.Customer, true); // 正確
db.SubmitChanges();
}
使用 Remove() 方法。一對多關聯中,必須先刪除「多」端明細,再刪除「一」端主檔,否則會違反外鍵限制:
var order = (from o in db.Orders
where o.CustomerID == "WARTH" && o.EmployeeID == 3
select o).First();
foreach (OrderDetail od in order.OrderDetails)
db.OrderDetails.Remove(od);
db.Orders.Remove(order);
db.SubmitChanges();
將 DataContext.Log 設定為任何 TextWriter(如 Console.Out、StreamWriter)即可記錄所有產生的 SQL 指令,利於偵錯與效能分析:
StreamWriter sw = new StreamWriter("log.txt", true);
db.Log = sw;
var q = db.Customers.Select(c => c).ToList();
sw.Close();
LINQ to SQL 內建樂觀並發支援(透過 IsVersion 或 UpdateCheck),衝突時拋出 ChangeConflictException。交易方面,SubmitChanges() 預設即包裝於隱性交易中;若需明確控制,可使用 TransactionScope 或 db.Transaction 屬性。