[VS2010] ADO.NET Entity Framework 新功能:永續儲存無知物件 (Persistence-Ignorant Object) Overview

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 資料庫。

 

image

 

8. 若在前面有啟動 SQL Profiler 監聽,則可以將它停止後,看看 Entity Framework 做了什麼事:

 

image

 

如何,是不是有想要尖叫的感覺呢?有了這樣的機制,Entity Framework 更向成熟的 ORM Framework 邁進一大步了。

 

由於這個功能太大,一回文章介紹不完,因此筆者下回再繼續深入介紹這個神奇的功能。

 

參考資料:

Updated Feature CTP Walkthrough: Code Only for Entity Framework

http://blogs.msdn.com/adonet/archive/2009/11/12/updated-feature-ctp-walkthrough-code-only-for-entity-framework.aspx

範例程式下載:

http://blogs.msdn.com/adonet/attachment/9921786.ashx