ADO.NET Entity Framework 的新功能:永續儲存無知物件。可以說是 Entity Framework 劃時代的新功能,顛覆一般的資料元件/DAL 與資料庫間的互動方式。
前一篇文章介紹了 ADO.NET Entity Framework 的模型優先設計 (Model First Design) 功能,有沒有覺得 .NET Framework 4.0 中的 ADO.NET Entity Framework 進步了很多呢?如果你這樣就滿足了,那接下來的東西你看了可能會像某啤酒廣告的女主角們那樣的尖叫~
一般在設計資料類別的時候,大多都是以一個純類別 (輕量級類別,只宣告必要的屬性代表欄位,並沒有和其他物件互動),包含幾個只針對資料做處理的屬性或方法等等,若要使用它和資料庫連結時,只能在裡面做一些 Property Mapping,並且另外撰寫資料存取的 ADO.NET 程式碼,這個動作只要是當過應用程式開發人員,就會覺得如呼吸般自然 … 長久以來都是如此,沒有一次例外。但 ADO.NET Entity Framework 2.0 試圖將這個局面顛覆過來,開發人員可以直接使用純類別物件來生成資料結構,並且由 Entity Framework 代為建立資料庫以及表格,這個方式可是以前 Visual Studio 開發人員前所未見的。這個方法在 Java 陣營中已經有一些實作品,而在 Java 陣營中對輕量級的資料物件一個特別的名詞:POJO (Plain-old Java Object),在 .NET 陣營也有一個相對的名詞,叫 POCO (Plain-old CLR Object),用途和 POJO 差不多,但微軟給了一個筆者覺得很難翻的名詞: Persistence-Ignorant Object,若直接翻譯會變成 “儲存無知型物件”,蠻拗口的,在官方翻譯未出來前,就暫時先翻成 “無差別儲存物件” (編按:在 "軟體構築美學" 一書中,將此詞翻譯為 "永續儲存無知",筆者認為這個詞翻的還不錯,所以引用) 好了,因為 Persistence-Ignorant Object 具有不必事先在 DBMS 中建立實體資料庫,就可以利用 Entity Framework 的 DDL Generation 功能將 POCO 物件結構轉換成實體資料結構以存入資料庫中的能力。
筆者認為永續儲存無知物件在 ADO.NET Entity Framework 乃至於 ADO.NET 甚至是整個 .NET Framework 的 Data Access 機制而言,都是一項劃時代的創新,它將資料存取元件設計的刻板印象完全的改變了,沒想到只使用幾個屬性的宣告就可以生成一個完成的資料結構,這可是許多開發人員夢寐以求的功能啊,筆者一開始看到這個功能時也是眼睛一亮,試過以後更覺得它是超級好物啊~
也許這樣看起來還是有點抽象,實際走一次例子就知道了。不過在做例子前,請先看看環境是否符合:
A. 已安裝 Visual Studio 2010
B. 下載並安裝 ADO.NET Entity Framework Feature CTP2 : http://www.microsoft.com/downloads/details.aspx?FamilyID=13FDFCE4-7F92-438F-8058-B5B4041D0F01&displayLang=en
C. 已安裝 SQL Server (最好是非 Express 版本,但 Express 版也無妨)
前置作業符合後,請依下列步驟執行:
1. 在 Visual Studio 中先建立一個 AdoEF_PocoExampleLibrary 類別庫專案,並且加入下列類別宣告程式碼 (可同在一個類別庫檔案或是分成個別的檔案):
public class Blog {
public Blog() { }
public int ID { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public User Owner { get; set; }
public ICollection<Post> Posts { get; set; }
}
public class Comment
{
public Comment() { }
public int ID { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public Person Author { get; set; }
public Post Post { get; set; }
public DateTime Created { get; set; }
public DateTime? Posted { get; set; }
}
public class Person
{
public int ID { get; set; }
public string Firstname { get; set; }
public string Surname { get; set; }
public string EmailAddress { get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Post
{
public Post() { }
public int ID { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public string PermaUrl { get; set; }
public DateTime Created { get; set; }
public DateTime? Posted { get; set; }
public User Author { get; set; }
public User Poster { get; set; }
public Blog Blog { get; set; }
public ICollection<Comment> Comments { get; set; }
public int BlogID { get; set; }
}
public class User : Person
{
public string Password { get; set; }
public ICollection<Blog> Blogs { get; set; }
public ICollection<Post> AuthoredPosts { get; set; }
public ICollection<Post> PostedPosts { get; set; }
}
2. 在 Visual Studio 中建立一個 AdoEF_PocoExampleClient 主控台專案,先新增一個 BloggingModel 類別,並將下列程式碼加入:
using System.Data.Entity;
using System.Data.EntityClient;
using System.Data.EntityModel;
using System.Data.Objects;
public class BloggingModel : ObjectContext
{
public BloggingModel(EntityConnection connection)
: base(connection)
{
DefaultContainerName = "BloggingModel";
}
public IObjectSet<AdoEF_PocoExampleLibrary.Blog> Blogs
{
get { return base.CreateObjectSet<AdoEF_PocoExampleLibrary.Blog>(); }
}
public IObjectSet<AdoEF_PocoExampleLibrary.Person> People
{
get { return base.CreateObjectSet<AdoEF_PocoExampleLibrary.Person>(); }
}
public IObjectSet<AdoEF_PocoExampleLibrary.Comment> Comments
{
get { return base.CreateObjectSet<AdoEF_PocoExampleLibrary.Comment>(); }
}
public IObjectSet<AdoEF_PocoExampleLibrary.Post> Posts
{
get { return base.CreateObjectSet<AdoEF_PocoExampleLibrary.Post>(); }
}
}
3. 在 AdoEF_PocoExampleClient 專案中加入一個 BlogDemo 類別,並將下列程式碼加入(其中紅字為資料庫連線字串,但 initial catalog 可以指定成目前還沒建的資料庫名稱,若你已經有 Blogs 資料庫,請將 initial catalog 的名稱改一下,否則執行以後,你原有的 Blogs 資料庫會被刪掉):
using System.Data.Entity;
using System.Data.EntityClient;
using System.Data.EntityModel;
using System.Data.Objects;
using Microsoft.Data.Objects;
class BlogDemo
{
public static void Run()
{
var builder = new ContextBuilder<BloggingModel>();
RegisterConfigurations(builder);
var connection = new SqlConnection("initial catalog=Blogs; integrated security=SSPI");
using (var ctx = builder.Create(connection))
{
if (ctx.DatabaseExists())
ctx.DeleteDatabase();
ctx.CreateDatabase();
var EfDesign =
new AdoEF_PocoExampleLibrary.Blog
{
Name = "EF Design",
Url = "http://blogs.msdn.com/efdesign/",
Owner = new AdoEF_PocoExampleLibrary.User
{
ID = 1,
Firstname = "Johnny",
Surname = "Miller",
EmailAddress = "johnnyM@hotmail.com",
Password = "Viking"
}
};
ctx.Blogs.AddObject(EfDesign);
var post = new AdoEF_PocoExampleLibrary.Post
{
Title = "Hello",
Blog = EfDesign,
PermaUrl = EfDesign.Url + "/2009/08/Hello",
Body = "....",
Author = EfDesign.Owner,
Poster = EfDesign.Owner,
Created = DateTime.Today,
Posted = DateTime.Today,
};
ctx.Posts.AddObject(post);
var comment = new AdoEF_PocoExampleLibrary.Comment
{
Title = "RE:" + post.Title,
Body = "Welcome to the world of blogging Johnny...",
Created = DateTime.Now,
Posted = DateTime.Now,
Post = post,
Author = new AdoEF_PocoExampleLibrary.Person
{
ID = 2,
Firstname = "Vincent",
Surname = "Chase",
EmailAddress = "vinny@hotmail.com",
}
};
ctx.Comments.AddObject(comment);
ctx.SaveChanges();
AdoEF_PocoExampleLibrary.Blog blog = ctx.Blogs.Single();
foreach (var entry in blog.Posts)
{
Console.WriteLine(entry.Title);
Console.WriteLine(entry.Author.Firstname);
}
}
}
static void RegisterConfigurations(ContextBuilder<BloggingModel> builder)
{
builder.Configurations.Add(new CommentConfiguration());
builder.Configurations.Add(new BlogConfiguration());
builder.Configurations.Add(new PostConfiguration());
builder.Configurations.Add(new PersonConfiguration());
builder.Configurations.Add(new UserConfiguration());
}
}
4. 在前一步的程式碼的下方,加入下列的程式碼:
public class CommentConfiguration : EntityConfiguration<AdoEF_PocoExampleLibrary.Comment>
{
public CommentConfiguration()
{
Property(c => c.ID).IsIdentity();
Property(c => c.Title).HasMaxLength(103).IsRequired();
Property(c => c.Body).IsRequired();
// 1 to * relationships
Relationship(c => c.Author).IsRequired();
Relationship(c => c.Post).IsRequired();
//Register some inverses
Relationship(c => c.Post).FromProperty(p => p.Comments);
Relationship(c => c.Author).FromProperty(u => u.Comments);
}
}
public class BlogConfiguration : EntityConfiguration<AdoEF_PocoExampleLibrary.Blog>
{
public BlogConfiguration()
{
Property(b => b.ID).IsIdentity();
Property(b => b.Name).HasMaxLength(100).IsRequired();
Relationship(b => b.Owner).IsRequired();
//Register some inverses
Relationship(b => b.Owner).FromProperty(u => u.Blogs);
Relationship(b => b.Posts).FromProperty(p => p.Blog);
}
}
public class PostConfiguration : EntityConfiguration<AdoEF_PocoExampleLibrary.Post>
{
public PostConfiguration()
{
// Make the PK store generated
Property(p => p.ID).IsIdentity();
// Convert some '0..1 to *' relationships into '1 to *'
Relationship(p => p.Author).IsRequired();
Relationship(p => p.Blog).IsRequired();
Relationship(p => p.Poster).IsRequired();
// Setup some facets
Property(p => p.Body).IsRequired();
Property(p => p.PermaUrl).HasMaxLength(200);
Property(p => p.Title).HasMaxLength(100);
// Register some Inverses
Relationship(p => p.Author).FromProperty(u => u.AuthoredPosts);
Relationship(p => p.Comments).FromProperty(c => c.Post);
Relationship(p => p.Poster).FromProperty(p => p.PostedPosts);
//BlogID is a FK property and Blog is a navigation property backed by this FK
Relationship(p => p.Blog).FromProperty(b => b.Posts).HasConstraint((p, b) => p.BlogID == b.ID);
}
}
public class PersonConfiguration : EntityConfiguration<AdoEF_PocoExampleLibrary.Person>
{
public PersonConfiguration()
{
Property(p => p.ID).IsIdentity();
Property(p => p.Firstname).HasMaxLength(100);
Property(p => p.Surname).HasMaxLength(100);
Property(p => p.EmailAddress).HasMaxLength(200);
MapHierarchy(
p => new
{
pid = p.ID,
email = p.EmailAddress,
fn = p.Firstname,
ln = p.Surname,
}
).ToTable("People");
}
}
public class UserConfiguration : EntityConfiguration<AdoEF_PocoExampleLibrary.User>
{
public UserConfiguration()
{
Property(u => u.Password).HasMaxLength(15).IsRequired();
Relationship(u => u.AuthoredPosts).FromProperty(p => p.Author);
Relationship(u => u.PostedPosts).FromProperty(p => p.Poster);
MapHierarchy(
u => EntityMap.Row(
EntityMap.Column(u.ID, " u i d"),
EntityMap.Column(u.Password)
)
).ToTable("Users");
}
}
5. 在 AdoEF_PocoExampleClient 的 Program.cs 中,加入下列的程式碼:
class Program
{
static void Main(string[] args)
{
BlogDemo.Run();
}
}
6. 若你所使用的資料庫是 SQL Server (非 Express 或 Compact Edition),可將 SQL Profiler 先啟動並進入監聽模式。
7. 按 F5 以除錯器執行或按 CTRL+F5 直接執行,正常情況下視窗會出現以後消失。此時請打開 SQL Server Management Studio 並連到本機伺服器中,即可以看到新的 Blogs 資料庫。
8. 若在前面有啟動 SQL Profiler 監聽,則可以將它停止後,看看 Entity Framework 做了什麼事:
如何,是不是有想要尖叫的感覺呢?有了這樣的機制,Entity Framework 更向成熟的 ORM Framework 邁進一大步了。
由於這個功能太大,一回文章介紹不完,因此筆者下回再繼續深入介紹這個神奇的功能。
參考資料:
Updated Feature CTP Walkthrough: Code Only for Entity Framework
範例程式下載: