程序代碼第一個數據批注

注意

僅限 EF4.1 及更新 版本 - 在 Entity Framework 4.1 中介紹了此頁面所討論的功能、API 等。 如果您使用舊版,則不適用部分或全部資訊。

此頁面的內容是改編自裘莉·勒曼<http://thedatafarm.com>()最初撰寫的文章。

Entity Framework Code First 可讓您使用自己的領域類別來代表 EF 依賴來執行查詢、變更追蹤和更新函式的模型。 Code First 會利用稱為「透過組態慣例」的程序設計模式。 Code First 會假設您的類別遵循 Entity Framework 的慣例,在此情況下,會自動找出如何執行其作業。 不過,如果您的類別未遵循這些慣例,您就能夠將組態新增至您的類別,以提供EF的必要資訊。

Code First 提供兩種方式,將這些組態新增至您的類別。 其中一個是使用稱為 DataAnnotations 的簡單屬性,第二個屬性是使用 Code First 的 Fluent API,其可讓您在程式代碼中以命令方式描述組態。

本文將著重於使用 DataAnnotations (在 System.ComponentModel.DataAnnotations 命名空間中) 來設定您的類別 – 醒目提示最常見的設定。 DataAnnotations 也由許多 .NET 應用程式瞭解,例如 ASP.NET MVC,可讓這些應用程式針對客戶端驗證使用相同的批注。

模型

我將示範 Code First DataAnnotations 與一對簡單的類別:部落格和文章。

    public class Blog
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public DateTime DateCreated { get; set; }
        public string Content { get; set; }
        public int BlogId { get; set; }
        public ICollection<Comment> Comments { get; set; }
    }

如同它們,部落格和文章類別會方便遵循程序代碼第一慣例,而且不需要任何調整,即可啟用 EF 相容性。 不過,您也可以使用批注,向EF提供有關其對應類別和資料庫的詳細資訊。

 

索引鍵

Entity Framework 依賴每個實體具有用於實體追蹤的索引鍵值。 Code First 的其中一個慣例是隱含索引鍵屬性;Code First 會尋找名為 「Id」 的屬性,或類別名稱和 「Id」 的組合,例如 「BlogId」。 這個屬性會對應至資料庫中的主鍵數據行。

部落格和 Post 類別都遵循此慣例。 如果他們沒有呢? 如果 Blog 改為使用 PrimaryTrackingKey 名稱,甚至使用 foo,該怎麼辦? 如果程式代碼第一次找不到符合此慣例的屬性,則會因為 Entity Framework 必須具有索引鍵屬性的需求而擲回例外狀況。 您可以使用索引鍵批注來指定要使用哪個屬性做為 EntityKey。

    public class Blog
    {
        [Key]
        public int PrimaryTrackingKey { get; set; }
        public string Title { get; set; }
        public string BloggerName { get; set;}
        public virtual ICollection<Post> Posts { get; set; }
    }

如果您使用程式代碼第一個資料庫產生功能,Blog 數據表會有一個名為 PrimaryTrackingKey 的主鍵數據行,預設也會定義為 Identity。

Blog table with primary key

複合索引鍵

Entity Framework 支援複合索引鍵 - 包含多個屬性的主鍵。 例如,您可以有 Passport 類別,其主鍵是 PassportNumber 和 IssuingCountry 的組合。

    public class Passport
    {
        [Key]
        public int PassportNumber { get; set; }
        [Key]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

嘗試在 EF 模型中使用上述類別會導致 InvalidOperationException

無法判斷類型 『Passport』 的複合主鍵順序。 使用 ColumnAttribute 或 HasKey 方法來指定複合主鍵的順序。

為了使用複合索引鍵,Entity Framework 會要求您定義索引鍵屬性的順序。 您可以使用資料行批註來指定順序來執行此動作。

注意

順序值是相對值(而非以索引為基礎),因此可以使用任何值。 例如,可以接受 100 和 200 取代 1 和 2。

    public class Passport
    {
        [Key]
        [Column(Order=1)]
        public int PassportNumber { get; set; }
        [Key]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }
        public DateTime Issued { get; set; }
        public DateTime Expires { get; set; }
    }

如果您有具有複合外鍵的實體,則必須指定用於對應主鍵屬性的相同數據行順序。

只有外鍵屬性內的相對順序必須相同,指派給 Order 的確切值不需要相符。 例如,在下列類別中,可以使用 3 和 4 取代 1 和 2。

    public class PassportStamp
    {
        [Key]
        public int StampId { get; set; }
        public DateTime Stamped { get; set; }
        public string StampingCountry { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 1)]
        public int PassportNumber { get; set; }

        [ForeignKey("Passport")]
        [Column(Order = 2)]
        public string IssuingCountry { get; set; }

        public Passport Passport { get; set; }
    }

必要

Required 注會告知 EF 需要特定屬性。

將 Required 新增至 Title 屬性會強制 EF (和 MVC) 確保屬性中有數據。

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

由於應用程式中沒有任何額外的程式代碼或標記變更,MVC 應用程式會執行客戶端驗證,甚至會使用 屬性和批註名稱動態建置訊息。

Create page with Title is required error

Required 屬性也會藉由使對應的屬性不可為 Null,來影響產生的資料庫。 請注意,[標題] 字段已變更為 “not null”。

注意

在某些情況下,即使需要 屬性,資料庫中的數據行可能無法為 Null。 例如,針對多個類型使用 TPH 繼承策略數據時,會儲存在單一數據表中。 如果衍生類型包含必要的屬性,則無法將數據行設為不可為 Null,因為階層中的所有類型都不會有這個屬性。

 

Blogs table

 

MaxLength 和 MinLength

MaxLengthMinLength 屬性可讓您指定其他屬性驗證,就像您使用 一Required樣。

以下是具有長度需求的 BloggerName。 此範例也會示範如何結合屬性。

    [MaxLength(10),MinLength(5)]
    public string BloggerName { get; set; }

MaxLength 註釋會將屬性的長度設定為 10,以影響資料庫。

Blogs table showing max length on BloggerName column

MVC 用戶端批註和 EF 4.1 伺服器端註釋都會接受此驗證,再次動態建置錯誤訊息:「BloggerName 字段必須是長度上限為 『10』 的字串或陣列類型。這一信息有點長。 許多註釋可讓您使用 ErrorMessage 屬性指定錯誤訊息。

    [MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

您也可以在必要批注中指定 ErrorMessage。

Create page with custom error message

 

NotMapped

程式代碼第一個慣例會指定資料庫中表示支援數據類型的每個屬性。 但這不一定是應用程式中的情況。 例如,您在 Blog 類別中可能有屬性,該屬性會根據 Title 和 BloggerName 字段建立程式碼。 該屬性可以動態建立,不需要儲存。 您可以使用 NotMapped 註釋來標記未對應至資料庫的任何屬性,例如這個 BlogCode 屬性。

    [NotMapped]
    public string BlogCode
    {
        get
        {
            return Title.Substring(0, 1) + ":" + BloggerName.Substring(0, 1);
        }
    }

 

ComplexType

在一組類別中描述您的網域實體,然後分層這些類別來描述完整的實體並不常見。 例如,您可以將名為 BlogDetails 的類別新增至您的模型。

    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

請注意, BlogDetails 沒有任何類型的索引鍵屬性。 在網域驅動設計中, BlogDetails 稱為值物件。 Entity Framework 會將值對象稱為複雜類型。  無法自行追蹤複雜類型。

不過,作為類別中的 Blog 屬性, BlogDetails 將會追蹤為物件的一 Blog 部分。 為了讓程式代碼先辨識這個,您必須將 BlogDetails 類別標示為 ComplexType

    [ComplexType]
    public class BlogDetails
    {
        public DateTime? DateCreated { get; set; }

        [MaxLength(250)]
        public string Description { get; set; }
    }

現在您可以在類別中 Blog 新增 屬性,以代表 BlogDetails 該部落格的 。

        public BlogDetails BlogDetail { get; set; }

在資料庫中, Blog 數據表會包含部落格的所有屬性,包括其 BlogDetail 屬性中包含的屬性。 根據預設,每個名稱前面都會加上複雜類型 「BlogDetail」。

Blog table with complex type

ConcurrencyCheck

ConcurrencyCheck 註可讓您在使用者編輯或刪除實體時,將一或多個屬性標示為用於資料庫中的並行檢查。 如果您一直在使用 EF 設計工具,這會與將 屬性的 ConcurrencyModeFixed設定為 一致。

讓我們看看如何將它新增至 屬性來BloggerName運作ConcurrencyCheck

    [ConcurrencyCheck, MaxLength(10, ErrorMessage="BloggerName must be 10 characters or less"),MinLength(5)]
    public string BloggerName { get; set; }

呼叫 時 SaveChanges ,由於 ConcurrencyCheck 欄位上的 BloggerName 註釋,該屬性的原始值將會用於更新中。 命令會藉由篩選索引鍵值,而且在 的原始值 BloggerName上篩選,嘗試找出正確的數據列。  以下是傳送至資料庫之 UPDATE 命令的重要部分,您可以在其中看到命令會更新具有 PrimaryTrackingKey 為 1 的數據列,而 BloggerName “Julie” 則是從資料庫擷取該部落格時的原始值。

    where (([PrimaryTrackingKey] = @4) and ([BloggerName] = @5))
    @4=1,@5=N'Julie'

如果有人同時變更該部落格的部落格名稱,此更新將會失敗,而且您將會收到 您需要處理的 DbUpdateConcurrencyException

 

TimeStamp

使用 rowversion 或 timestamp 欄位進行並行檢查比較常見。 但是,只要屬性的類型是位元組數位,就可以使用更特定的TimeStamp註釋,而不是使用ConcurrencyCheck註釋。 程序代碼會先將屬性視為 Timestamp 屬性 ConcurrencyCheck ,但也會確保程式代碼第一次產生的資料庫欄位不可為 Null。 在指定的類別中,您只能有一個 timestamp 屬性。

將下列屬性新增至 Blog 類別:

    [Timestamp]
    public Byte[] TimeStamp { get; set; }

會導致程式代碼先在資料庫數據表中建立不可為 Null 的時間戳數據行。

Blogs table with time stamp column

 

數據表和數據行

如果您讓 Code First 建立資料庫,您可能想要變更它正在建立的數據表和數據行的名稱。 您也可以搭配現有的資料庫使用 Code First。 但是,您網域中的類別和屬性名稱不一定符合資料庫中數據表和數據行的名稱。

我的類別會依慣例命名 Blog ,程式代碼會先假設這會對應至名為的 Blogs數據表。 如果不是這種情況,您可以使用 屬性來指定資料表 Table 的名稱。 例如,註釋指定數據表名稱為 InternalBlogs

    [Table("InternalBlogs")]
    public class Blog

Column 註更善於指定對應數據行的屬性。 您可以指定名稱、資料類型,甚至指定資料行出現在資料表中的順序。 以下是 屬性的 Column 範例。

    [Column("BlogDescription", TypeName="ntext")]
    public String Description {get;set;}

請勿混淆 Column 的屬性 TypeName 與 DataType DataAnnotation。 DataType 是 UI 所使用的批注,由 Code First 忽略。

以下是重新產生數據表之後的數據表。 資料表名稱已從複雜類型變更為 InternalBlogs 和資料 Description 行現在 BlogDescription為 。 由於已在批注中指定名稱,因此程式代碼會先不使用以複雜類型名稱開頭數據行名稱的慣例。

Blogs table and column renamed

 

DatabaseGenerated

重要的資料庫功能是擁有計算屬性的能力。 如果您要將 Code First 類別對應至包含計算數據行的數據表,則不希望 Entity Framework 嘗試更新這些數據行。 但您確實希望 EF 在插入或更新數據之後,從資料庫傳回這些值。 您可以使用 DatabaseGenerated 批註來標記 類別中的這些屬性以及 Computed 列舉。 其他列舉為 NoneIdentity

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime DateCreated { get; set; }

當程式代碼第一次產生資料庫時,您可以使用位元組或時間戳數據行上產生的資料庫,否則只有在指向現有的資料庫時,才應該使用此資料庫,因為程式代碼第一次無法判斷計算數據行的公式。

您預設會讀取上述內容,表示整數的索引鍵屬性將會成為資料庫中的識別密鑰。 這與將 設定DatabaseGeneratedDatabaseGeneratedOption.Identity為 相同。 如果您不想將它設定為識別金鑰,您可以將值設定為 DatabaseGeneratedOption.None

 

索引

注意

僅限 EF6.1 及更新 版本 - 已在 Index Entity Framework 6.1 中引進屬性。 如果您使用舊版,本節中的資訊不適用。

您可以使用 IndexAttribute 在一或多個數據行上建立索引。 將 屬性新增至一或多個屬性會導致 EF 在建立資料庫時,於資料庫中建立對應的索引,或者如果您使用 Code First 移轉,請建立對應的 CreateIndex 呼叫。

例如,下列程式代碼會導致在資料庫中數據表的數據Posts行上Rating建立索引。

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index]
        public int Rating { get; set; }
        public int BlogId { get; set; }
    }

根據預設,索引會命名 為IX_<屬性名稱> (上述範例中的IX_Rating)。 您也可以指定索引的名稱。 下列範例指定索引應該命名為 PostRatingIndex

    [Index("PostRatingIndex")]
    public int Rating { get; set; }

根據預設,索引不是唯一的 IsUnique ,但您可以使用具名參數來指定索引應該是唯一的。 下列範例會在的登入名稱上引進唯一 User索引。

    public class User
    {
        public int UserId { get; set; }

        [Index(IsUnique = true)]
        [StringLength(200)]
        public string Username { get; set; }

        public string DisplayName { get; set; }
    }

多欄索引

跨多個數據行的索引是在指定數據表的多個索引批注中使用相同名稱來指定。 當您建立多資料行索引時,您必須指定索引中數據行的順序。 例如,下列程式代碼會在 上 Rating 建立多重數據行索引,並 BlogId 呼叫 IX_BlogIdAndRatingBlogId 是索引中的第一個數據行,也是 Rating 第二個數據行。

    public class Post
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        [Index("IX_BlogIdAndRating", 2)]
        public int Rating { get; set; }
        [Index("IX_BlogIdAndRating", 1)]
        public int BlogId { get; set; }
    }

 

關聯性屬性:InverseProperty 和 ForeignKey

注意

此頁面提供使用數據批注在 Code First 模型中設定關聯性的相關信息。 如需 EF 中關聯性的一般資訊,以及如何使用關聯性存取及操作數據,請參閱 關聯性與導覽屬性。*

程序代碼第一慣例會負責模型中最常見的關聯性,但在某些情況下需要協助。

變更 類別中 Blog 索引鍵屬性的名稱時,會建立與其關聯 Post性的問題。 

產生資料庫時,程式代碼會先看到 BlogId Post類別中的屬性,並藉由符合類別名稱加上 Id的慣例來辨識該屬性,做為類別的 Blog 外鍵。 但是部落格類別中沒有 BlogId 屬性。 解決方法是在 中 Post 建立導覽屬性,並使用 ForeignKey DataAnnotation協助程式代碼先瞭解如何建置兩個類別之間的關聯性(使用 Post.BlogId 屬性),以及如何在資料庫中指定條件約束。

    public class Post
    {
            public int Id { get; set; }
            public string Title { get; set; }
            public DateTime DateCreated { get; set; }
            public string Content { get; set; }
            public int BlogId { get; set; }
            [ForeignKey("BlogId")]
            public Blog Blog { get; set; }
            public ICollection<Comment> Comments { get; set; }
    }

資料庫中的條件約束會顯示和Posts.BlogId之間的InternalBlogs.PrimaryTrackingKey關聯性。 

relationship between InternalBlogs.PrimaryTrackingKey and Posts.BlogId

InverseProperty當您在類別之間有多個關聯性時,會使用 。

在課程中 Post ,您可能想要追蹤誰撰寫了部落格文章,以及誰編輯了部落格文章。 以下是 Post 類別的兩個新的導覽屬性。

    public Person CreatedBy { get; set; }
    public Person UpdatedBy { get; set; }

您必須在這些屬性所參考的 Person 類別中新增 。 類別 Person 具有返回的導覽屬性 Post,一個用於人員所撰寫的所有文章,另一個用於該人員更新的所有文章。

    public class Person
    {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<Post> PostsWritten { get; set; }
            public List<Post> PostsUpdated { get; set; }
    }

程式代碼首先無法自行比對兩個類別中的屬性。 的資料庫數據表 Posts 應該有一個外鍵給 CreatedBy 人員,一個用於 UpdatedBy 人員,但程式代碼會先建立四個外鍵屬性: Person_IdPerson_Id1CreatedBy_IdUpdatedBy_Id

Posts table with extra foreign keys

若要修正這些問題,您可以使用 InverseProperty 批注來指定屬性的對齊方式。

    [InverseProperty("CreatedBy")]
    public List<Post> PostsWritten { get; set; }

    [InverseProperty("UpdatedBy")]
    public List<Post> PostsUpdated { get; set; }

PostsWritten因為 Person 中的 屬性知道這是指型別Post,所以它會建置與 的Post.CreatedBy關聯性。 同樣地, PostsUpdated 將會連線到 Post.UpdatedBy。 而程式代碼會先建立額外的外鍵。

Posts table without extra foreign keys

 

摘要

DataAnnotations 不僅可讓您在程式代碼的第一個類別中描述用戶端和伺服器端驗證,還可讓您根據其慣例來增強甚至更正程式代碼會先對類別做出的假設。 使用 DataAnnotations,您不僅可以驅動資料庫架構產生,還可以將程式代碼第一個類別對應至既有的資料庫。

雖然它們非常有彈性,但請記住,DataAnnotations 只提供您在程式代碼第一個類別上所能進行的最常見組態變更。 若要設定某些邊緣案例的類別,您應該查看替代組態機制Code First的 Fluent API 。