LINQ to SQL 實體類
只要使用了適當的屬性 (attribute),LINQ to SQL允許你使用任何類去展現數據:
1: [Table] 2: 3: public class Customer 4: 5: { 6: 7: [Column(IsPrimaryKey=true)] 8: 9: public int ID; 10: 11: [Column] 12: 13: public string Name; 14: 15: }
[Table]屬性存在於System.Data.Linq.Mapping命名空間中,它將會告訴LINQ to SQL此類型的一個對象實例代表數據庫表中的一行.默認情況下,它假設表名和類名是一致,如果不一致,我們也可以使用下面的做法來完成映射:
[Table (Name=”Customers”)]
一個被標記了[Table]屬性的類稱為一個LINQ to SQL實體.它非常接近設置完全匹配數據庫當中的表結構.
[Column]屬性標記了一個映射到數據庫列的字段或者屬性. 如果列名和字段名或屬性名不同, 我們也可以用以下的做法完成映射:
[Column Name=”FullName”]
public string Name;
IsPrimaryKey屬性指示該列是否是表中的主鍵列.為了維護對象的一致性以及更新和寫回數據庫, 它是必須的. 我們還可以定義公開屬性去關聯私有字段, 而不是直接定義共有字段,這樣我們可以在屬性訪問器裡面添加驗證邏輯. 如果選擇了這種方式, 你可以選擇讓LINQ to SQL在讀取數據庫數據的時候跳過屬性訪問器直接將數據寫入字段:
1: string _name; 2: 3: [Column (Storage=“_name”)] 4: 5: public string Name 6: 7: { get { return _name; } set { _name =value; } }
[Column (Storage=”_name”)]指示LINQ to SQL直接將數據寫入_name字段(而不是通過Name屬性). 由於LINQ to SQL使用反射因此可以允許字段被定義為私有的.
DataContext
當你定義了實體類之後, 你可以通過實例化DataContext然後調用GetTable開始進行查詢操作, 如下所示:
1: var dc = new DataContext (“cx string…”); 2: 3: Table customers = dc.GetTable (); 4: 5: Customer cust = customers.OrderBy (c =>c.Name).First(); 6: 7: cust.Name = “the new name”; 8: 9: dc.SubmitChanges();
一個DataContext對象會對它實例化的所有實體類進行追蹤, 在其生命周期內, 一個DataContext永遠都不會包含2個指向表中同一行的實體對象.
查看以下示例:
1: var dc = new DataContext (“connectionString”); 2: 3: Table customers = dc.GetTable (); 4: 5: Customer a = customers.OrderBy (c => c.Name).First(); 6: 7: Customer b =customers.OrderBy (c => c.ID).First();
假設a和b返回的都是數據庫中的同一條記錄,這裡就會有很多很有趣的結果產生.首先,當第二條查詢執行的時候,它訪問了數據庫並且獲取了一條單筆的記錄,然後取得主鍵並且跟DataContext實例當中的實體Cache進行了一下比較, 結果發現有一條匹配的記錄已經存在, 因此查詢直接反復了那條已經存在的記錄,沒有改變任何值.這意味著,如果在第一條查詢執行之後有人改變了該記錄的某個值,那麼第二條查詢並不會獲得這個最新值.這樣設計的目的是為了避免一些不可意料的邊界影響以及並發管理.如果你更新了某個實體的屬性,但是並沒有調用SubmitChanges,這些值將不會被改寫.
第二個隨之而來的結果是你不能顯式的使用一個所有列的子集去填充一個實體類.例如,你可以使用以下的任意一種方法去查詢數據並只返回客戶名:
1: customers.Select (c => c.Name); 2: 3: customers.Select (c => new { Name = c.Name } ); 4: 5: customers.Select (c => new YourDefinedCustomerType { Name = c.Name } );
而使用以下的做法是行不通的:
1: customers.Select (c => new Customer {Name = c.Name } );
這是因為Customer實體僅僅是部分屬性被賦值,因此如果下次你有另外一個查詢要返回所有的列,你得到將會是Cache中那個只有Name屬性被賦值的實體.
[在一個多層應用程序中,你不能在中間層中使用一個單一的靜態DataContext實例去處理所有的請求,因為DataContext並不是線程安全的.相反的,中間層方法必須根據每一次的客戶端請求去創建新的DataContext對象]
自動實體生成
你可以根據已知的數據庫schema,通過SqlMeta命令行工具或者VS當中的LINQ to SQL設計器來自動生成對應的LINQ to SQL實體類. 這些工具都會生成partial的實體類, 因此你可以在另外一個分離的文件中添加額外的邏輯.
作為額外的獎勵, 你還將得到一個強類型的DataContext類, 它是一個DataContext的子類型並通過屬性包裝了每一個Table對應的實體類. 通過這個強類型DataContext,你就不再需要調用GetTable了:
1: var dataContext = new MyTypedDataContext (“connectionString”); 2: 3: Table customers = dataContext.Customers; 4: 5: Console.WriteLine (customers.Count());
或者簡單的使用:
1: Console.WriteLine (dataContext.Customers.Count());
LINQ to SQL設計器會在合適的地方使用復數標識符,在這個例子中,它使用的是dataContext.Customers而不是dataContext.Customer – 雖然SQL Table和實體類名都是Customer.
關聯
實體類生成工具還做了一項非常有用的工作,對於每一個在數據庫中已經定義了的關系,生成工具都會在兩邊的實體類中自動生成對應的屬性.例如,假設我們定義了一個客戶和采購表的一對多的關系:
1: create table Customer 2: 3: ( 4: 5: ID int not null primary key, 6: 7: Name varchar(30) not null 8: 9: ) 10: 11: create table Purchase 12: 13: ( 14: 15: ID int not null primary key, 16: 17: CustomerID int references Customer (ID), 18: 19: Description varchar(30) not null, 20: 21: Price decimal not null 22: 23: )
如果我們使用了自動生成的實體類的話,我們可以類似下面這樣編寫查詢:
1: var dataContext = new MyTypedDataContext (“connectionString”); 2: 3: Customer cust1 = dataContext.Customers.OrderBy (c => c.Name).First(); 4: 5: foreach (Purchase p in cust1.Purchases) 6: 7: Console.WriteLine (p.Price); 8: 9: Purchase cheapest = dataContext.Purchases.OrderBy (p => p.Price).First(); 10: 11: Customer cust2 = cheapest.Customer;
可以看到我們可以通過customer引用到purchase,同樣也可以通過purchase引用到對應的customer. 並且如果cust1和cust2剛好指向的是同一條客戶記錄, cust1==cust2則會返回true.
現在讓我們來查看一下Customer實體上自動生成的Purchases屬性的標記:
1: [Association (Storage=“_Purchases”, 2: 3: OtherKey=“CustomerID”)] 4: 5: public EntitySet Purchases 6: 7: { get {…} set {…}}
一個EntitySet類似是一個預定義的內置Where語句,用於查詢關聯的實體.[Association]屬性提供了LINQ to SQL編寫此查詢所需的信息.和其他類型的查詢一樣,這個查詢是延遲執行的,這意味著只有當你開始枚舉相關結果集的時候查詢才會真正執行.
而在Purchase實體類也有對應的Customer屬性, 如下所示:
1: [Association (Storage=“_Customer”, 2: 3: ThisKey=“CustomerID”, 4: 5: IsForeignKey=true)] 6: 7: public Customer Customer { get {…} set{…} }
雖然這個屬性的類型是Customer,但其後的對應字段_Customer是EntityRef類型(通過EntityRef.Entity得到Customer實體類),EntityRef實現了延遲執行功能,因此只有當你真正需要Customer數據的時候它才會從數據庫當中讀取對應的行.
LINQ to SQL的延遲執行
LINQ to SQL的延遲執行與本地查詢在語義上有點不同,那就是當一個子級查詢出現在一個Select表達式裡面的時候
1. 如果是本地查詢, 你會有兩次的延遲查詢. 這意味著如果你只是枚舉外層的結果序列, 而沒有去枚舉內部的序列, 那麼內部的子查詢永遠都不會執行
2. 而如果是LINQ to SQL,子查詢將會在外部查詢被執行的時候就執行了, 這可以避免額外的往還.
1: var dataContext = new CustomDataContext (“myConnectionString”); 2: 3: var query = from c in dataContext.Customers 4: 5: select 6: 7: from p in c.Purchases 8: 9: select new{ c.Name, p.Price }; 10: 11: foreach (var customerPurchaseResults inquery) 12: 13: foreach (var namePrice in customerPurchaseResults) 14: 15: Console.WriteLine (namePrice.Name + ” spent “ + namePrice.Price);
任何EntitySets都會顯式的在一次往返中加載所有的數據:
1: var query = from c in dataContext.Customers 2: 3: select new { c.Name, c.Purchases }; 4: 5: foreach (var row in query) 6: 7: foreach (Purchase p in row.Purchases) 8: 9: Console.WriteLine (row.Name + ” spent “ + p.Price);
但如果我們先擁有query變量,而是直接使用下面這樣的做法:
1: foreach (Customer c in dataContext.Customers) 2: 3: foreach (Purchase p in c.Purchases) // 數據庫往返 4: 5: Console.WriteLine (c.Name + ” spent “ + p.Price);
這種情況延遲加載將會被應用,並在每一個循環中都會和數據庫交互.雖然可能引起性能問題, 但當你希望有選擇性的執行內部循環的時候,這種做法可以帶來你所需要的好處,例如:
1: foreach (Customer c in dataContext.Customers) 2: 3: if (myWebService.yourMethod (c.ID)) // 商業邏輯判斷 4: 5: foreach (Purchase p in c.Purchases) // 數據庫往返 6: 7: Console.WriteLine (“output something”);
DataLoadOptions
DataLoadOption有兩個主要的用途:
1.允許你預先設置一個EntitySet關聯的過濾器
2.允許你立即加載對應的EntitySets以便減少
預先過濾
下面的例子演示如何使用DataLoadOptions的AssociateWith方法:
1: DataLoadOptions options = new DataLoadOptions(); 2: 3: options.AssociateWith 4: 5: (c => c.Purchases.Where (p => p.Price > 456)); 6: 7: dataContext.LoadOptions =options;
代碼段指示DataContext實例總是使用給定的斷言來過濾customer對應的purchase采購訂單.AssociateWith並不會改變延遲執行的語義, 它只是簡單的對一個特定的關系應用了一個過濾器而已.
熱加載(Eager Loading)
DataLoadOptions的另外一個用途是對特定的EntitySets熱加載其對應的parents, 假設你想在一次數據庫往返中加載所有的customers及其對應的purchase,示例如下:
1: DataLoadOptions options = new DataLoadOptions(); 2: 3: options.LoadWith (c =>c.Purchases); 4: 5: dataContext.LoadOptions = options; 6: 7: foreach (Customer c in dataContext.Customers) 8: 9: foreach (Purchase p in c.Purchases) 10: 11: Console.WriteLine (c.Name + ” bought a “ + p.Description);
該代碼段表示當讀取Customer的時候,其對應的Purchases也應該在同一時間被加載, 你甚至可以進一步記載PurchaseItem:
1: options.LoadWith (c => c.Purchases); 2: 3: options.LoadWith (p =>p.PurchaseItems);
我們也可以將AssociateWith和LoadWith組合在一起使用, 如下所示:
1: options.LoadWith (c => c.Purchases); 2: 3: options.AssociateWith (c => c.Purchases.Where (p => p.Price > 456));
這表示當加載Customer的時候同時加載那些Price金額大於456的Purchases.
更新
LINQ to SQL會保持你對實體做的變更痕跡並且允許你調用DataContext.SubmitChanges將它們寫回數據庫.Table<>類上的InsertOnSubmit和DeleteOnSubmit分別用於新增或者刪除表中的一行. 以下示例演示如何使用它們:
1: var dataContext = new CustomDataContext (“connectionString”); 2: 3: Customer cust = new Customer { ID=1000, Name=“Bloggs” }; 4: 5: dataContext.Customers.InsertOnSubmit(cust); 6: 7: dataContext.SubmitChanges();
我們也可以在讀取行資料之後,更新然後刪除它:
1: var dataContext = new CustomDataContext (“connectionString”); 2: 3: Customer cust = dataContext.Customers.Single(c => c.ID == 1000); 4: 5: cust.Name = “Bloggs2″; 6: 7: dataContext.SubmitChanges(); // 更新customer 8: 9: dataContext.Customers.DeleteOnSubmit (cust); 10: 11: dataContext.SubmitChanges(); // 刪除customer
DataContext.SubmitChanges收集所有自從DataContext創建以來(或者上次調用SubmitChanges以來)的變更,然後執行對應的SQL語句將他們寫回數據庫.所有的語句都將被放在一個Transaction當中來運行.
你也可以通過調用Add將一個新行加入到一個EntitySet當中,LINQ to SQL會自動設置對應的外鍵:
1: var p1 = new Purchase { ID=100, Description=“Bike”,Price=500 }; 2: 3: var p2 = new Purchase { ID=101, Description=“Tools”,Price=100 }; 4: 5: Customer cust = dataContext.Customers.Single(c => c.ID == 1); 6: 7: cust.Purchases.Add (p1); 8: 9: cust.Purchases.Remove (p2); 10: 11: dataContext.SubmitChanges();
在此例子中, LINQ to SQL自動將1寫入到各個新的purchase中的CustomerID列, 原因就是因為我們定義的Purchases屬性:
1: [Association (Storage=“_Purchases”, 2: 3: OtherKey=“CustomerID”)] 4: 5: public EntitySet Purchases 6: 7: { get {…} set {…}
如果實體類是由Visual Studio的設計器或者SqlMeta命令行工具自動生成的話, Customer和Purchase之間的關聯會有相應的代碼自動同步維護。換句話說, 賦值到Purchase.Customer屬性會自動將一個新的Customer加入到Customer.Purchases這個EntitySet中,反之亦然. 因此我們可以將之前的例子改寫為:
1: var dataContext = new MyTypedDataContext (“connectionString”); 2: 3: Customer cust = dataContext.Customers.Single 4: 5: (c => c.ID == 1); 6: 7: new Purchase { ID=100, Description=“Bike”, Price=500, 8: 9: Customer=cust}; 10: 11: new Purchase { ID=101, Description=“Tools”, Price=100, 12: 13: Customer=cust}; 14: 15: dataContext.SubmitChanges(); // 新增purchase
當你從一個EntitySet當中移除一條記錄的時候,它的外鍵將會自動被設為null. 以下代碼將兩條purchase記錄和它們對應的customer解除關系:
1: var dataContext = new MyTypedDataContext (“connectionString”); 2: 3: Customer cust = dataContext.Customers.Single(c => c.ID == 1); 4: 5: cust.Purchases.Remove 6: 7: (cust.Purchases.Single (p => p.ID == 100)); 8: 9: cust.Purchases.Remove 10: 11: (cust.Purchases.Single (p => p.ID == 101)); 12: 13: dataContext.SubmitChanges( ); // 提交到SQL Server
這裡調用SubmitChanges會試圖將數據庫中對應的CustomerID列的值設為null, 因為數據庫中此列必須為nullable, 更進一步, 實體類中對應的屬性也必須是nullable的, 否則將會得到一個異常. 如果你想把記錄從數據庫中直接刪除, 則應該使用DeleteOnSubmit將他們從Table<>中刪除, 如下所示:
1: Customer cust = dataContext.Customers.Single(c => c.ID == 1); 2: 3: var dc = dataContext; 4: 5: dc.Purchases.DeleteOnSubmit 6: 7: (dc.Purchases.Single (p => p.ID == 100)); 8: 9: dc.Purchases.DeleteOnSubmit 10: 11: (dc.Purchases.Single (p => p.ID == 101)); 12: 13: dataContext.SubmitChanges(); // 提交到SQL Server數據庫