Заметки к данным Code First

Примечание.

ТОЛЬКО EF4.1 — функции, API и т. д., рассмотренные на этой странице, были представлены в Entity Framework 4.1. Если вы используете более раннюю версию, некоторые или все эти сведения не применяются.

Содержимое на этой странице адаптировано из статьи, первоначально написанной Джули Лерман (<http://thedatafarm.com>).

Entity Framework Code First позволяет использовать собственные классы домена для представления модели, которую EF использует для выполнения запросов, отслеживания изменений и обновления функций. Code First использует шаблон программирования, называемый "соглашением по конфигурации". В коде сначала предполагается, что классы следуют соглашениям Entity Framework, и в этом случае автоматически вы узнаете, как выполнять свою работу. Однако если классы не соответствуют этим соглашениям, у вас есть возможность добавлять конфигурации в классы для предоставления EF необходимых сведений.

Code First предоставляет два способа добавления этих конфигураций в классы. Один из них использует простые атрибуты с именем DataAnnotations, а второй — использование API Fluent Code First, который предоставляет способ описания конфигураций императивно в коде.

В этой статье рассматривается использование dataAnnotations (в пространстве имен System.ComponentModel.DataAnnotations), чтобы настроить классы — выделение наиболее распространенных необходимых конфигураций. ДанныеAnnotations также понимаются рядом приложений .NET, таких как ASP.NET MVC, что позволяет этим приложениям использовать те же заметки для проверки на стороне клиента.

Модель

Я продемонстрировать код 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; }
    }

Как они есть, классы Блога и Post удобно следуют первому соглашению о коде и не требуют настройки для обеспечения совместимости EF. Однако можно также использовать заметки для предоставления дополнительных сведений о классах и базе данных, с которыми они сопоставляются.

 

Ключ.

Entity Framework использует все сущности, имеющие ключевое значение, используемое для отслеживания сущностей. Одно соглашение Code First — это неявные ключевые свойства; Code First будет искать свойство с именем "Id", или сочетание имени класса и идентификатора, например "BlogId". Это свойство сопоставляется с столбцом первичного ключа в базе данных.

Классы Blog и Post соответствуют этому соглашению. Что делать, если они этого не сделали? Что делать, если блог использовал имя 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; }
    }

Если вы используете функцию создания базы данных первого кода, таблица блога будет иметь столбец первичного ключа с именем 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, что требуется определенное свойство.

Добавление в свойство Title приведет к принудительному добавлению EF (и MVC), чтобы убедиться, что свойство содержит данные в нем.

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

Без дополнительных изменений кода или разметки в приложении приложение MVC будет выполнять проверку на стороне клиента, даже динамически создавая сообщение с помощью имен свойств и заметок.

Create page with Title is required error

Обязательный атрибут также влияет на созданную базу данных, делая сопоставленное свойство не допускаемым значением NULL. Обратите внимание, что поле Title изменилось на "не null".

Примечание.

В некоторых случаях столбец в базе данных может быть не допускаемым значением NULL, даже если это свойство является обязательным. Например, при использовании данных стратегии наследования TPH для нескольких типов хранятся в одной таблице. Если производный тип содержит обязательное свойство, столбец нельзя сделать не допускаемым значение NULL, так как не все типы в иерархии будут иметь это свойство.

 

Blogs table

 

MaxLength и MinLength

Атрибуты MaxLength позволяют указывать дополнительные проверки свойств, как и при использованииRequired.MinLength

Вот имя блоггера с требованиями к длине. В примере также показано, как объединять атрибуты.

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

Заметка MaxLength повлияет на базу данных, задав длину свойства 10.

Blogs table showing max length on BloggerName column

Заметка на стороне клиента MVC и заметка на стороне сервера EF 4.1 будут учитывать эту проверку, снова динамически создавая сообщение об ошибке: "Поле BlogName должно быть строкой или типом массива с максимальной длиной "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

Первое соглашение кода определяет, что каждое свойство, являющееся поддерживаемым типом данных, представлено в базе данных. Но это не всегда так в приложениях. Например, у вас может быть свойство в классе Блога, которое создает код на основе полей Title и BlogName. Это свойство можно создать динамически и не нужно хранить. Вы можете пометить любые свойства, которые не сопоставляются с базой данных с помощью заметки 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.

Давайте посмотрим, как ConcurrencyCheck работает, добавив его в BloggerName свойство.

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

При SaveChanges вызове из-за ConcurrencyCheck заметки в BloggerName поле исходное значение этого свойства будет использоваться в обновлении. Команда попытается найти правильную строку, отфильтровав не только значение ключа, но и исходное значение BloggerName.  Ниже приведены критически важные части команды UPDATE, отправляемой в базу данных, где вы увидите, что команда обновит строку с значением 1 и BloggerName "Джули", PrimaryTrackingKey которая была исходным значением при извлечении этого блога из базы данных.

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

Если кто-то изменил имя блога для этого блога в то же время, это обновление завершится ошибкой, и вы получите DbUpdateConcurrencyException , который вам потребуется обработать.

 

TimeStamp

Чаще всего используются поля rowversion или timestamp для параллелизма проверка. Но вместо использования ConcurrencyCheck заметки можно использовать более конкретную TimeStamp заметку, если тип свойства является массивом байтов. Сначала код будет обрабатывать Timestamp свойства так же, как ConcurrencyCheck свойства, но также гарантирует, что поле базы данных, которое сначала создает код, не является пустым. В данном классе можно использовать только одно свойство метки времени.

Добавление следующего свойства в класс Блога:

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

результаты кода сначала создают столбец метки времени, не допускающего значения NULL, в таблице базы данных.

Blogs table with time stamp column

 

Таблица и столбец

Если вы позволяете коду сначала создать базу данных, может потребоваться изменить имя таблиц и столбцов, которые он создает. Вы также можете использовать 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 — это заметка, используемая для пользовательского интерфейса и игнорируемая кодом 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. Добавление атрибута к одному или нескольким свойствам приведет к созданию соответствующего индекса в базе данных при создании базы данных или формирования шаблонов соответствующих вызовов CreateIndex , если вы используете code First Migrations.

Например, следующий код приведет к созданию индекса в Rating столбце Posts таблицы в базе данных.

    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_<property name> (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_BlogIdAndRating. BlogId — первый столбец в индексе и 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 и распознает его по соглашению, которое соответствует имени класса плюс идентификатор, как внешний ключ к классу Blog . Но в классе блога нет BlogId свойства. Решением для этого является создание свойства навигации в Post dataAnnotation и использование 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; }
    }

Ограничение в базе данных показывает связь между InternalBlogs.PrimaryTrackingKey и Posts.BlogId

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 базы данных должна иметь один внешний ключ для пользователя и один для CreatedByUpdatedBy пользователя, но код сначала создаст четыре свойства внешнего ключа: Person_Id, Person_Id1, CreatedBy_Id и UpdatedBy_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 предоставляют только наиболее часто необходимые изменения конфигурации, которые можно внести в первые классы кода. Чтобы настроить классы для некоторых пограничных вариантов, следует искать альтернативный механизм настройки API Fluent Code First.