[Entity Framework] 跨出 Code First 開發第一步 (搭配MVC5為例)

跨出 Code First 開發第一步 (搭配MVC5為例)

 

A. 開發環境建立

首先需要安裝Entity Framework,可以透過Package Manager Console或Manager NuGet Packages兩種方式進行安裝。使用Package Manager Console方式的朋友,請輸入 Install-Package EntityFramework 即可完成安裝;若使用Manager NuGet Packages的朋友,請搜尋EntityFramework並點選安裝即可,結果如下圖所示。

image

 

B. Model的建立資料

首先Code First的開發方式,顧名思義就是透過類別(Class)的定義來產生相對應的資料庫Table Schema。所以一開始的工作就是要建立類別及其關聯。相信大家對於Database First的開發方式都較為熟悉,所以筆者試著先將最終我們透過Code First 產生出的 ER-Model 繪出,讓大家先有個整體概念並思考要如何透過類別的設計來映射到Table Schema的每個環節,好讓Entity Framework輔助我們達到Code First的目標。

image_thumb2_thumb

1. 建立類別(設定主鍵 Primary Key)

EF針對各類別 Primary Key 的自動剖析規則如下:

* 屬性名稱為 Id類別名稱Id 將被視為主鍵(Key)

* 主鍵屬性型態為 數字GUID 時,將被自動設定為 Identity 欄位

* 若要避免主鍵被自動設定成Identity欄位

   可透過 [DatabaseGenerated(DatabaseGeneratedOption.None)]標籤調整

 

Customer Data Model

屬性名稱為 Id 將被視為主鍵(Key),且主鍵屬性型態為 數字 時,將被自動設定為 Identity 欄位。

using System;
using System.Collections.Generic;

namespace CodeFirstWebApplication.Models
{
    public class Customer
    {
        // Primary Key
        public int Id { get; set; } 

        public string Email { get; set; }

        public string Password { get; set; }

        public string Name { get; set; }

        public DateTime RegisterOn { get; set; }
    }
}

Order Data Model

屬性名稱為 類別名稱Id 將被視為主鍵(Key),且主鍵屬性型態為 數字 時,將被自動設定為 Identity 欄位。

namespace CodeFirstWebApplication.Models
{
    public class Order
    {
        // Primary Key
        public int OrderId { get; set; }

        public string Memo { get; set; }
    }
}

Product Data Model

屬性名稱為 類別名稱Id 將被視為主鍵(Key),且主鍵屬性型態為 數字 時,將被自動設定為 Identity 欄位。由於我們的需求是希望商品Id由用戶自行輸入一組數字,所以我們將透過 [DatabaseGenerated(DatabaseGeneratedOption.None)]標籤來避免主鍵被自動設定成Identity欄位。

using System.ComponentModel.DataAnnotations.Schema;

namespace CodeFirstWebApplication.Models
{
    public class Product
    {
        // Primary Key
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int ProductId { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }

        public int Price { get; set; }
    }
}

 

2. 針對各類別之相對應關係進行設定(外來鍵 Foreign Key)

EF針對各類別 Foreign Key 的自動剖析規則如下:

* 屬性名稱 = 導覽屬性名稱+參考主鍵屬性名稱,將被視為 Foreign Key

* 屬性名稱 = 參考主鍵屬性名稱,將被視為 Foreign Key

   實務上只會依循同一種規則來設計所有Model,請避免混用以免造成不必要的誤解

 

Customer Data Model

其中Orders屬性為導覽屬性(Navigation Property),表示存放與本類別相關聯的類別。以本實例來說,顧客(Customer)與訂單(Order)是一對多的關係,表示單一顧客會擁有多筆訂單,所以該顧客所有訂單物件都將存放於 ICollection<Order> Orders 中。而 virtual 前置詞表示 Lazy Loading,表示只有在使用到Customer.Orders的時候才會從DB取得相關訂單資訊。

using System;
using System.Collections.Generic;

namespace CodeFirstWebApplication.Models
{
    public class Customer
    {
        // Primary Key
        public int Id { get; set; } 

        public string Email { get; set; }

        public string Password { get; set; }

        public string Name { get; set; }

        public DateTime RegisterOn { get; set; }


        // Navigation Properties
        public virtual ICollection<Order> Orders { get; set; }
    }
}

Order Data Model

屬性名稱 = 導覽屬性名稱+參考主鍵屬性名稱 ex. Customer+Id = CustomerId,將被視為 Foreign Key

屬性名稱 = 參考主鍵屬性名稱 ex. ProductId,將被視為 Foreign Key

namespace CodeFirstWebApplication.Models
{
    public class Order
    {
        // Primary Key
        public int OrderId { get; set; }

        public string Memo { get; set; }


        // Foreign Key
        public int CustomerId { get; set; }

        // Foreign Key
        public int ProductId { get; set; }


        // Navigation Property
        public virtual Customer Customer { get; set; }

        // Navigation Property
        public virtual Product Product { get; set; }
    }
}

Product Data Model

商品(Product)與訂單(Order)是一對多的關係,表示單一商品會擁有多筆訂單,所以該商品所有訂單物件都將存放於 ICollection<Order> Orders 中。而 virtual 前置詞表示 Lazy Loading,表示只有在使用到Product.Orders的時候才會從DB取得相關訂單資訊。

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace CodeFirstWebApplication.Models
{
    public class Product
    {
        // Primary Key
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int ProductId { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }

        public int Price { get; set; }


        // Navigation Properties
        public virtual ICollection<Order> Orders { get; set; }
    }
}

 

3. 針對類別屬性(Table Cloumn)進行裝飾

目前僅將各Data Model的主鍵及外來鍵設定完畢。回想一下當我們在設計Table Schema還有哪些資訊要設定呢? 沒錯就是基本的欄位型別、欄位大小及是否可允許Null值存在。筆者將就這些基本的設定逐一透過以下方式讓Entity Framework知道,並且產出我們所期望的Table Schema。

 

* 欄位型別

欄位型別絕大部分都可以透過類別屬性的型態來自動映射。其中又可透過 DataType 標籤進一步的指定資料型別,舉例來說,當我們加註 DataType.EmailAddress 於屬性中,該屬性在資料的呈現時會自動加上 mailto: 的link。

    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

* 欄位大小

當類別屬性型別為字串時,我們可透過 StringLength 標籤來設定欄位大小,而資料驗證時也將會依照該設定來檢核字串字數是否超過上限。

[StringLength(50, ErrorMessage = "名字不可超過50字元")]
public string Name { get; set; }

* 是否允許Null值

依照類別屬性型別是否可接受Null值作為判斷依據。假設型別為字串時,由於字串是可允許Null值,所以在映射到Table後該欄位亦可接受Null值;若是要將該欄位設定為不允許Null值時,我們可透過 Required 標籤來實現。請注意Price屬性在映射到Table欄位時將不允許Null值情況,因為int本身就是不允許Null值,所以不需要再加註Required 標籤來實現;若想將Price屬性改成允許Null值情況,只要將 int 調整為 int? 即可。

        [Required]
        public string Name { get; set; }

        public string Description { get; set; }

        public int Price { get; set; }

其實有許多標籤可以讓我們針對複雜的Data Model屬性進行細部設定,有興趣的朋友可以參考以下資訊。

Creating a More Complex Data Model

 

C. 建立 Database Context

整個操作Entity Framewor的核心物件就是 Database Context 類別,扮演著資料提供者的角色。我們將建立一個繼承至 System.Data.Entity.DbContext 的類別 StoreContext,針對各Data Model加入 DbSet 屬性;在Entity Framework中DbSet就如同Table,而Data Model就如同Table中的每一筆資料。

using CodeFirstWebApplication.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace CodeFirstWebApplication.DAL
{
    public class StoreContext : DbContext
    {
        // Coustructors
        public StoreContext() : base("StoreContext")
        {
        }


        // Properties
        public DbSet<Customer> Customers { get; set; }

        public DbSet<Order> Orders { get; set; }

        public DbSet<Product> Products { get; set; }


        // Methods
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        }
    }
}

注意: 由於筆者不希望EF在產生Table名稱時自動變為複數(ex. Customers, Orders, Products),所以覆寫了OnModelCreating方法並在其中加註 modelBuilder.Conventions.Remove<PluralizingTableNameConvention>() 來達成我們的目標。

 

D. 建立測試資料或預設資料

由於開發階段會時常會調整Data Model並透過EF來建立(刪除與重建)DB,若是每次都要手動寫入測試(預設)資料必定對於開發人員造成不少困擾,所以我們將透過以下方式讓EF在建立(刪除與重建)DB後自動執行自訂之行為。新增一個名稱為StoreInitializer類別並繼承System.Data.Entity.DropCreateDatabaseIfModelChanges<StoreContext>,接著覆寫Seed方法並在內容加上測試資料或預設資料即可。

using CodeFirstWebApplication.Models;
using System;
using System.Collections.Generic;

namespace CodeFirstWebApplication.DAL
{
    public class StoreInitializer : System.Data.Entity.DropCreateDatabaseIfModelChanges<StoreContext>
    {
        protected override void Seed(StoreContext context)
        {
            var customers = new List<Customer>
            {
            new Customer{Name="Carson1",Email="Carson1@gmail.com",RegisterOn=DateTime.Parse("2005-09-01")},
            new Customer{Name="Carson2",Email="Carson2@gmail.com",RegisterOn=DateTime.Parse("2005-09-02")},
            new Customer{Name="Carson3",Email="Carson3@gmail.com",RegisterOn=DateTime.Parse("2005-09-03")},
            new Customer{Name="Carson4",Email="Carson4@gmail.com",RegisterOn=DateTime.Parse("2005-09-04")},
            };

            customers.ForEach(s => context.Customers.Add(s));
            context.SaveChanges();
            var products = new List<Product>
            {
            new Product{ProductId=1050,Name="Chemistry",Price=3,},
            new Product{ProductId=4022,Name="Microeconomics",Price=3,},
            new Product{ProductId=4041,Name="Macroeconomics",Price=3,},
            new Product{ProductId=1045,Name="Calculus",Price=4,},
            new Product{ProductId=3141,Name="Trigonometry",Price=4,},
            new Product{ProductId=2021,Name="Composition",Price=3,},
            new Product{ProductId=2042,Name="Literature",Price=4,}
            };
            products.ForEach(s => context.Products.Add(s));
            context.SaveChanges();
            var orders = new List<Order>
            {
            new Order{CustomerId=1,ProductId=1050,Memo="memo11"},
            new Order{CustomerId=1,ProductId=4022,Memo="memo12"},
            new Order{CustomerId=1,ProductId=4041,Memo="memo13"},
            new Order{CustomerId=2,ProductId=1045,Memo="memo14"},
            new Order{CustomerId=2,ProductId=3141,Memo="memo15"},
            new Order{CustomerId=2,ProductId=2021,Memo="memo16"},
            new Order{CustomerId=3,ProductId=1050},
            new Order{CustomerId=4,ProductId=1050},
            new Order{CustomerId=4,ProductId=4022,Memo="memo17"},
          
            };
            orders.ForEach(s => context.Orders.Add(s));
            context.SaveChanges();
        }
    }
}

 

接著需要告訴EF使用這個初始的類別去建立預設資料。

image

 <entityFramework>
    <contexts>
      <context type="CodeFirstWebApplication.DAL.StoreContext, CodeFirstWebApplication">
        <databaseInitializer type="CodeFirstWebApplication.DAL.StoreInitializer, CodeFirstWebApplication" />
      </context>
    </contexts>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="v11.0" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>

 

 

E. 建立連線字串

在此使用的是LocalDB資料庫,其中 AttachDbFilename=|DataDirectory|\CodeFirstDb.mdf 表示資料庫將被建置於App_Data資料夾中。其實我們也可以不主動設定ConnectionString,EF將會依照Context來為我們建立出一個預設的ConnectionString來,請參考Code First to a New Database

<connectionStrings>
    <add name="StoreContext" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\CodeFirstDb.mdf;Initial Catalog=CodeFirstDb;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>

 

F. 透過Db Context實踐資料的CRUD

1. 建立Customer Controller

image

image

PS 若新增失敗,請先編譯程式後再新增一次

image

由於筆者選擇的Scaffold就是針對EF來產生Controller及View

所以Controller中就是使用StoreContext來對DB進行存取作業

image

 

2. 修改_Layout.cshtml,加入顯示Customer資料的連結(指到Customer Controller的Index Action)

image

image

3. 在程式執行前筆者希望驗證以下的事項:

    A. 何時EF會建立DB

         為了確定DB的狀況,首先登入SQL Server

         image

         目前只掛載2個LocalDB資料庫

         image

    B. 是否真的會進入StoreInitializer類別,建立初始資料

         可以建立中斷點來檢查是否真的有執行到這段

         image

4. 執行程式

   此時DB尚未被EF建立,所以也不會進入StoreInitializer類別,建立初始資料

   image

   當點選Customer連結後,因使用到StoreContext所以觸發EF產生DB。

   我們可以看到EF已經幫忙建立出CodeFirstDb的LocalDB,其中的Table皆依照Data Model的定義而開設的

   image

   由於連線字串中設定AttachDbFilename=|DataDirectory|\CodeFirstDb.mdf

   表示資料庫將被建置於App_Data資料夾中

   所以在專案的App_Data資料夾中產生了EF建立的mdf檔(預設不加入專案,請點選上方Show All Files選項)。

   image

   接著確實進入StoreInitializer類別,建立初始資料

  image

   畫面上呈現的資料,就是透過StoreInitializer類別建立的初始資料

   (此處只著重功能測試,故不另調整View頁面來去除Password欄位的顯示)

   image

   再來簡單測試CRUD,此範例就先測試新增就好

   image

   的確可以透過StoreContext將資料寫進資料庫,並可從DB撈出顯示與頁面上

image

 

G. 資料欄位異動處理

當然在開發專案的過程中絕對不會如此的精準,所以時常有機會需要異動Data Model,此時我們就要利用以下的方式來同步更新DB,筆者將透過一個簡單異動來串起所有的步驟。

1. 異動Customer Data Model

    目前Customer類別如下所示,筆者預計加入一個新的屬性來記錄顧客住址

             

 public class Customer
    {
        // Primary Key
        public int Id { get; set; }

        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }

        [Required]
        public string Password { get; set; }

        [Required]
        [StringLength(50, ErrorMessage = "名字不可超過50字元")]
        public string Name { get; set; }

        [DataType(DataType.Date)]
        public DateTime RegisterOn { get; set; }


        // Navigation Properties
        public virtual ICollection<Order> Orders { get; set; }
    }

   調整過後的Customer Data Model (加入Address屬性)

             

public class Customer
    {
        // Primary Key
        public int Id { get; set; }

        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }

        [Required]
        public string Password { get; set; }

        [Required]
        [StringLength(50, ErrorMessage = "名字不可超過50字元")]
        public string Name { get; set; }

        [DataType(DataType.Date)]
        public DateTime RegisterOn { get; set; }

        // 新加入的屬性以記錄顧客住址
        public String Address { get; set; }

        // Navigation Properties
        public virtual ICollection<Order> Orders { get; set; }
    }

2. 啟動 Code First Migrations

    點選 TOOLS – Library Package Manager – Package Manager Console

    image

   輸入 Enable-Migrations 執行

   (若專案中不只一個 DbContex,需在指令後加上–ContextTypeName DbContext類別名稱)

   image

   此時會在專案中新增Migrations資料夾,並產生2個檔案

   image

   A. <時間>_InitialCreate.cs

       在初次使用Migration時會建立此檔,其表示從空白DB轉移至目前使用中DB的異動過程。

                

public partial class InitialCreate : DbMigration
    {
        public override void Up()
        {
            CreateTable(
                "dbo.Customer",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Email = c.String(),
                        Password = c.String(nullable: false),
                        Name = c.String(nullable: false, maxLength: 50),
                        RegisterOn = c.DateTime(nullable: false),
                    })
                .PrimaryKey(t => t.Id);
            
            CreateTable(
                "dbo.Order",
                c => new
                    {
                        OrderId = c.Int(nullable: false, identity: true),
                        Memo = c.String(),
                        CustomerId = c.Int(nullable: false),
                        ProductId = c.Int(nullable: false),
                    })
                .PrimaryKey(t => t.OrderId)
                .ForeignKey("dbo.Customer", t => t.CustomerId, cascadeDelete: true)
                .ForeignKey("dbo.Product", t => t.ProductId, cascadeDelete: true)
                .Index(t => t.CustomerId)
                .Index(t => t.ProductId);
            
            CreateTable(
                "dbo.Product",
                c => new
                    {
                        ProductId = c.Int(nullable: false),
                        Name = c.String(nullable: false),
                        Description = c.String(),
                        Price = c.Int(nullable: false),
                    })
                .PrimaryKey(t => t.ProductId);
            
        }
        
        public override void Down()
        {
            DropForeignKey("dbo.Order", "ProductId", "dbo.Product");
            DropForeignKey("dbo.Order", "CustomerId", "dbo.Customer");
            DropIndex("dbo.Order", new[] { "ProductId" });
            DropIndex("dbo.Order", new[] { "CustomerId" });
            DropTable("dbo.Product");
            DropTable("dbo.Order");
            DropTable("dbo.Customer");
        }
    }

   B. Configuration.cs

       其中若 AutomaticMigrationsEnabled 修改為 true 時,可不用建立Migration紀錄

       直接輸入 Update-Database 執行,將DB依照目前Data Model狀態進行自動更新

                

internal sealed class Configuration : DbMigrationsConfiguration<CodeFirstWebApplication.DAL.StoreContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
            ContextKey = "CodeFirstWebApplication.DAL.StoreContext";
        }

        protected override void Seed(CodeFirstWebApplication.DAL.StoreContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
        }
    }

3. 新增Migration紀錄

    輸入 Add-Migration AddAddress 執行,其中AddAddress為任意名稱,供識別這次異動資訊而命名

    image

    此時新增一筆Migration的紀錄AddAddress(取用Add-Migration的識別名稱),記錄著上下版本的差異

    image

              

public partial class AddAddress : DbMigration
    {
        public override void Up()
        {
            AddColumn("dbo.Customer", "Address", c => c.String());
        }
        
        public override void Down()
        {
            DropColumn("dbo.Customer", "Address");
        }
    }

 

4. 更新資料庫

    最後輸入 Update-Database 執行更新,亦可加上 -Verbose 讓畫面顯示異動的SQL語法

    其中發現除了新增Customer表單Address欄位外,亦紀錄了Migration資訊於 __MigratioinHistory中

    image

 

5. 驗證DB是否如預期調整

    可以看到Custormer表單已經加上Address欄位,且在__MigrationHistory也記載了此次Migration紀錄

    image

H. 後記

雖然花了不少的時間在實作,但對於剛接觸這門技術的我,總算有了一點的感覺。希望未來能夠有適合的專案讓筆者實際操練一下,也期許自己在面對各項新的領域都可以花時間邊做邊紀錄,一方面能讓像我懵懂的人有個粗淺的參考資訊,也好讓未來的我有喚醒記憶的機會。

 

參考資料

http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application

http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/creating-a-more-complex-data-model-for-an-asp-net-mvc-application

http://msdn.microsoft.com/en-us/data/jj193542


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !