在上一篇中,我們從理論和概念上詳細的了解了LINQ的第二種架構“解釋查詢”。在這接下來的二個篇章中,我們將使用LINQ to SQL和Entity Framework來實踐“解釋查詢”,學習這些技術的關鍵特性。在本系列文章中,我不准備事無巨細的討論LINQ to SQL和Entity Framework的方方面面,畢竟那樣需要太多的篇幅,也會讓我們從LINQ上面轉移注意力,況且,園子里也有不少介紹LINQ to SQL和Entity Framework的好文章。我們在此關注的是LINQ to SQL和Entity Framework中的”LINQ”部分,並會比較這兩種技術的相同和不同之處。通過我們之前介紹的LINQ知識還有將來會討論的更多LINQ Operators,相信閱者能針對LINQ to SQL和Entity Framework寫出優雅高效的查詢。為了簡單清晰,文中有些地方對LINQ to SQL和Entity Framework進行了縮寫,分別為:L2S和EF。
LINQ to SQL和Entity Framework之關聯
LINQ to SQL和Entity Framework都是一種包含LINQ功能的對象關系映射技術。他們之間的本質區別在於EF對數據庫架構和我們查詢的類型實行了更好的解耦。使用EF,我們查詢的對象不再是完全對應數據庫架構的C#類,而是更高層的抽象:Entity Data Model。這為我們提供了額外的靈活性,但是在性能和簡單性上面也會有所損失。
LINQ to SQL由C#團隊開發並在.NET Framework 3.5中發布,而Entity Framework由ADO.NET團隊開發並作為.NET Framework 3.5 Service Pack 1的一部分發布。此后,LINQ to SQL由ADO.NET團隊接手,其結果是:在.NET 4.0中,ADO.NET團隊更加專注於EF的改進,相對來說,LINQ to SQL的改進要小得多。
LINQ to SQL和Entity Framework各有所長,LINQ to SQL是一個輕量級的ORM框架,旨在為Microsoft SQL Server數據庫提供快速的應用程序開發,其優點是易於使用、簡單、高性能。而Entity Framework的優點在於:其為創建數據庫架構和實體類之間的映射提供了更好的靈活性,它還通過提供程序支持除了SQL Server之外的第三方數據庫。
EF 4.0一個非常受歡迎的改進是它現在支持與LINQ to SQL幾乎同樣的查詢功能。這意味着我們在系列文章中的LINQ-to-db查詢可以同時適用於EF 4.0和L2S。而且,這也使得L2S成為我們學習使用LINQ查詢數據庫的理想技術,因為其保持了對象關系方面的簡單性,並且我們學習到的查詢原則和技術同樣適用於EF。
LINQ to SQL實體類
L2S 允許我們使用任何類來表示數據,只要我們為類添加了合適的Attribute(特性)裝飾,比如:
[Table]
public class Customer
{
[Column(IsPrimaryKey = true)]
public int ID;
[Column]
public string Name;
}
[Table] 特性定義在System.Data.Linq.Mapping名字空間中,它告訴L2S該類型的對象代表了數據庫表里的一行數據。默認情況下,它假設表名和類名相同,當他們不同時,我們就可以指定具體的表名,如下:
[Table (Name="Customers")]
L2S把這種經過[Table]特性裝飾的類成為實體類。一個實體類的結構必須匹配它表示的數據庫表,這樣才能生成可以正確執行的SQL腳本。
[Column] 特性指定一個字段或屬性映射到數據庫表的一列,如果列名與字段名/屬性名不相同,我們可以指定具體的映射列名:
[Column(Name = "FullName")]
public string Name;
我們可以在[Column]特性中指定IsPrimaryKey屬性表示該列為表的主鍵,這對於保持對象標識、往數據庫寫入更新是必須的。
除了直接定義public字段,我們也可以定義private字段和public屬性,這樣我們就能在屬性存取時加入驗證邏輯。此時,為了性能考慮,我們可以告訴L2S當從數據庫存取數據時,繞過屬性存取器而直接將值寫入private字段。當然,前提是我們認為數據庫中的值是正確的,不需要經過屬性存取器中的驗證邏輯。
private string name = string.Empty;
// Column(Storage = "name") 告訴L2S當從數據庫生成實體時直接將數據寫入name字段,而不通過set訪問器
[Column(Storage = "name")]
public string Name
{
get { return name; }
set { if(value.Length > 5) name = value; }
}
可以看到,在使用LINQ to SQL時,我們首先要參照數據庫的結構來創建各種必須的實體類,這當然不是一種令人愉快的事情。好在,我們可以通過Visual Studio(新增一個”LINQ to SQL Classes” Item)或SqlMetal命令行工具來自動生成實體類。
Entity Framework實體類
和LINQ to SQL一樣,Entity Framework允許我們使用任何類來表示數據(盡管我們必須實現特定的接口來完成諸如導航屬性等功能)。比如,下面的EF實體類表示一個customer,它被映射到數據庫的customer表:
[EdmEntityType (NamespaceName="EFModel", Name="Customer")]
public partial class Customer
{
[EdmScalarProperty(EntityKeyProperty = true, IsNullable= false )]
public int ID { get; set; }
[EdmScalarProperty(EntityKeyProperty = false, IsNullable = false)]
public string Name { get; set; }
}
但和L2S不同的是,這樣的一個類並不能獨立工作。記住:在使用EF時,我們並不是直接查詢數據庫,而是查詢一個更高層的模型,該模型叫做Entity Data Model(EDM)。所以我們需要某種方法來描述EDM,這通常是由一個以.edmx為擴展名的XML文件來完成的,它包含了一下三個部分:
- 概念模型,用來描述EDM並且和數據庫完全隔離
- 存儲模型,用來描述數據庫架構
- 映射規范,用來描述概念模型如何映射到存儲模型
創建一個.edmx文件的最簡單方法是在Visual Studio中添加一個”ADO.NET Entity Data Model” 項目,然后按照向導提示來從數據庫生成EDM。這種方法不但生成了.edmx文件,還為我們生成了實體類,EF中的實體類對應EDM的概念模型。
設計器為我們生成的EDM初始時包含了表和實體類之間簡單的1:1映射關系,當然,我們可以通過設計器或編輯.edmx文件來調整我們的EDM。下面就是我們可以完成的一些工作:
- 映射多個表到一個實體
- 映射一個表到多個實體
- 通過ORM領域流行的三種標准策略來映射繼承的類型
這三種繼承策略包括:
- 表到層次類型(Table per hierarchy):單個表映射到一個完整的類繼承層次結構。表中的一個類型辨別列用來指示每一行數據應該映射到何種類型。
- 表到類型(Table per type):單個表映射到單個類型,這意味着繼承類型會被映射到多個表。當我們查詢一個entity時,EF通過生成SQL JOIN來合並所有的基類型。
- 表到具體類型(Table per concrete type):單獨的表映射到每個具體類型,這意味着一個基類型映射到多個表,當我們查詢基類型的entity時,EF會生成SQL UNION來合並數據。
DataContext和ObjectContext
一旦我們定義好了實體類(EF還需定義EDM),我們就可以開始使用LINQ進行查詢了。第一步就是通過制定連接字符串來初始化一個DataContext(L2S)或ObjectContext(EF)。
var l2sContext = new DataContext("database connection string");
var efContext = new ObjectContext("entity connection string");
需要了解的是,直接初始化DataContext/ObjectContext是一種底層的訪問方式,但它很好的說明了這些類的工作方式。通常情況下,我們會使用類型化的context(通過繼承DataContext/ObjectContext),詳細情況稍后就會討論。
對於L2S,我們傳入數據庫連接字符串;而對於EF,我們需要傳入實體(entity)連接字符串,它同時包含了數據庫連接字符串和查找EDM的額外信息。如果你通過Visual Studio創建了EDM,你會在app.config文件中找到針對該EDM的實體連接字符串,比如:
<connectionStrings>
<add name="testEntities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=localhost;initial catalog=test;integrated security=True;multipleactiveresultsets=True;App=EntityFramework"" providerName="System.Data.EntityClient"/>
</connectionStrings>
之后我們就可以通過調用GetTable(L2S)或CreateObjectSet(EF)來獲得查詢對象。下面的示例使用了我們上面創建的Customer實體類:
var context = new DataContext("Data Source=LUIS-MSFT; Initial Catalog=test; Integrated Security=SSPI;");
Table<Customer> customers = context.GetTable<Customer>();
Console.WriteLine(customers.Count()); // 表中的行數
Customer cust = customers.Single(c => c.ID == 1); // 獲取ID為1的Customer
下面是EF中的等價代碼,可以看到,除了Context的構建和查詢對象的獲取有所不同,后面的LINQ查詢都是一樣的:
var context = new ObjectContext(ConfigurationManager.ConnectionStrings["testEntities"].ConnectionString);
context.DefaultContainerName = "testEntities";
ObjectSet<Customer> customers = context.CreateObjectSet<Customer>();
Console.WriteLine(customers.Count()); // 表中的行數
Customer cust = customers.Single(c => c.ID == 1); // 獲取ID為1的Customer
一個DataContext/ObjectContext對象有兩個作用。其一是工廠作用,我們通過它來創建查詢對象,另外,它會跟蹤我們對entity所做的任何修改,所以我們可以把修改結果保存到數據庫。下面的代碼就是更新customer的示例:
// Update Customer with L2S
Customer cust = customers.OrderBy(c => c.Name).First();
cust.Name = "Updated Name";
context.SubmitChanges();
// Update Customer with EF, Calling SaveChanges instead
Customer cust = customers.OrderBy(c => c.Name).First();
cust.Name = "Updated Name";
context.SaveChanges();
強類型contexts
任何時候都去調用GetTable<>()或CreateObjectSet<>()並不是一件讓人愉快的事情,一個更好的方式是為特定的數據庫創建DataContext/ObjectContext的子類,在子類中為各個entity添加屬性,這就是強類型的context,如下:
class LifePoemContext : DataContext
{
public LifePoemContext(string connectionString) : base(connectionString) { }
public Table<Customer> Customers
{
get { return GetTable<Customer>(); }
}
//... 為其他table創建相應的屬性
}
// Same thing for EF
class LifePoemContext : ObjectContext
{
public LifePoemContext(EntityConnection connection) : base(connection) { }
public ObjectSet<Customer> Customers
{
get { return CreateObjectSet<Customer>(); }
}
//... 為其他table創建相應的屬性
}
之后,我們就可以通過使用屬性來寫出更加簡潔優雅的代碼了:
var context = new LifePoemContext("database connection string");
Console.WriteLine(context.Customers.Count());
如果你是使用Visual Studio來創建”LINQ to SQL Classes”或”ADO.NET Entity Data Model”,它會自動為我們生成強類型的context。設計器同時還會完成其他的工作,比如對標識符使用復數形式,在我們的例子中,它是context.Customers而不是context.Customer,即使SQL表名和實體類都叫Customer。
對象跟蹤/Object tracking
一個DataContext/ObjectContext實例會跟蹤它創建的所有實體,所以當你重復請求表中相同的行時,它可以給你返回之前已經創建的實體。換句話說,一個context在它的生存期內不會為同一行數據生成兩個實例。你可以在L2S中通過設置DataContext對象的ObjectTrackingEnabled屬性為false來取消這種行為。在EF中,你可以基於每一種類型進行設置,如:context.Customers.MergeOption = MergeOption.NoTracking; 需要注意的是,禁用Object tracking同時也會阻止你想數據庫提交更新。
為了說明Object tracking,假設一個Customer的名字按字母排序排在首位,同時它的ID也是最小的。那么,下面的代碼,a和b將會指向同一個對象:
var context = new testEntities(ConfigurationManager.ConnectionStrings["testEntities"].ConnectionString);
Customer a = context.Customers.OrderBy(c => c.Name).First();
Customer b = context.Customers.OrderBy(c => c.ID).First();
Console.WriteLine(object.ReferenceEquals(a, b)); // output: True
這會導致幾個有意思的結果。首先,讓我們考慮當L2S或EF在遇到第二個query時到底會發生什么。它從查詢數據庫開始,然后獲取ID最小的那一行數據,接着就會從該行讀取主鍵值並在context的實體緩存中查找該主鍵。如果找到,它會直接返回緩存中的實體而不更新任何值。所以,如果在這之前其他用戶更新了該Customer的Name,新的Name也會被忽略。這對於防止意外的副作用和保持一致性至關重要,畢竟,如果你更新了Customer對象但是還沒有調用SubmitChanges/SaveChanges,你是不會希望你的更新會被另外一個查詢覆蓋的吧。
第二個結果是在你不能明確把結果轉換到一個實體類形,因為在你只選擇一行數據的部分列時會引起不必要的麻煩。例如,如果你只想獲取Customer的Name時:
// 下面任何一種方法都是可行的
context.Customers.Select(c => c.Name);
context.Customers.Select(c => new { Name = c.Name } );
context.Customers.Select(c => new MyCustomerType { Name = c.Name } );
// 但下面這種方法會引起麻煩
context.Customers.Select(c => new Customer { Name = c.Name });
原因在於Customer實體只是部分屬性被獲取,這樣下一次如果你查詢Customer的所有列時,可是context從緩存中返回的的對象只有部分屬性被賦值。
關聯/Associations
實體生成工具還為我們完成了一項非常有用的工作。對於我們定義在數據庫中的每個關聯(relationship),它會在關聯的兩邊添加恰當的屬性,讓我們可以使用關聯來進行查詢。比如,假設Customer和Order表存在一對多的關系:
Create table Customer
(
ID int not null primary key,
Name varchar(30) not null
)
Create table Orders
(
ID int not null primary key,
CustomerID int references Customer (ID),
OrderDate datetime,
Price decimal not null
)
通過自動生成的實體類形,我們可以寫出如下的查詢:
//獲取第一個Custoemr的所有Orders
Customer cust1 = context.Customers.OrderBy(c => c.Name).First();
foreach (Order o in cust1.Orders)
Console.WriteLine(o.Price);
//獲取訂單額最小的那個Customer
Order lowest = context.Orders.OrderBy(o => o.Price).First();
Customer cust2 = lowest.Customer;
並且,如果cust1和cust2正好是同一個Customer時,他們會指向同一對象:cust1 == cust2會返回true。
讓我們來進一步查看Customer實體類中自動生成的Orders屬性的簽名:
// With L2S
[Association(Name="Customer_Order", Storage="_Orders", ThisKey="ID", OtherKey="CustomerID")]
public EntitySet<Order> Orders { get {...} set {...} }
// With EF
[EdmRelationshipNavigationProperty("testModel", "FK__Orders__Customer__45BE5BA9", "Orders")]
public EntityCollection<Order> Orders { get {...} set {...} }
一個EntitySet或EntityCollection就如同一個預先定義的query,通過內置的Where來獲取相關的entities。[Association]特性給予L2S必要的信息來構建這個SQL query;[EdmRelationshipNavigationProperty]特性告知EF到EDM的何處去查找當前關聯的信息。
和其他類型的query一樣,這里也會采用延遲執行。對於L2S,一個EntitySet會在你對其進行枚舉時生成;而對於EF,一個EntityCollection會在你精確調用其Load方法時生成。
下面是Orders.Customer屬性(位於關聯的另一邊):
// With L2S
[Association(Name="Customer_Order", Storage="_Customer", ThisKey="CustomerID", OtherKey="ID", IsForeignKey=true)]
public Customer Customer { get {...} set {...} }
盡管屬性類型是Customer,但它底層的字段(_Customer)卻是EntityRef類型的:private EntityRef<Customer> _Customer; EntityRef實現了延遲裝載(deferred loading),所以直到你真正使用它時Customer才會從數據庫中獲取出來。
EF以相同的方式工作,不同的是你必需調用EntityReference對象的Load方法來裝載Customer屬性,這意味着EF必須同時公開真正的Customer對象和它的EntityReference包裝者,如下:
// With EF
[EdmRelationshipNavigationProperty("testModel", "FK__Orders__Customer__45BE5BA9", "Customer")]
public Customer Customer { get {...} set {...} }
public EntityReference<Customer> CustomerReference
我們也可以讓EF按照L2S的方式來工作,當我們設置如下屬性后,EF的EntityCollections和EntityReference就會自動實現延遲裝載,而不需要明確調用其Load方法。
context.ContextOptions.LazyLoadingEnabled = true;
在下一篇LINQ to SQL和Entity Framework(下)中,我們會討論學習這兩種LINQ-to-db技術的更多細節和值得關注的地方。