В прошлой части были рассмотрены соглашения, на основе которых Entity Framework Code First генерирует таблицы для хранения данных. Но что делать, если требования бизнес-логики не совпадают с соглашениями по умолчанию? Можно ли сделать свойство MySupertProperty первичным ключом?
Для таких ситуация возможно два типа решений:
- Использование атрибутов, указываемых для классов Модели и их свойств.
- Описание параметров и соотношений с использованием вызовов Entity Framework Fluent API.
Указанные подходы имеют принципиальное различие. В первом случае, необходимо изменить исходный код классов Модели и указать в нём необходимые атрибуты. В противовес, для второго варианта достаточно создать описание в виде вызовов специального Fluent API в специальном методе класса контекста доступа к базе данных. Код Модели при этом остается неизменным.
Также стоит отметить, что указанные варианты не взаимоисключающие. Это значит, что атрибуты могут быть дополнены с помощью описания Fluent API. Последний, кстати, имеет больший вес в определении параметров соотношения классов Модели и таблиц.
Рассмотрим оба варианта более подробно и в этой части разберем первый из них.
Атрибуты, описывающие соотношения в Code First
Собственно данный способ наиболее прост для понимания. Достаточно назначить необходимый атрибут классу или свойству и это соответствующим образом отразится при генерации таблиц в базе данных.
Давайте посмотрим, какие параметры соотношений можно установить дополнительно. Обратите внимание на важный момент: указывать повторно варианты по умолчанию нет необходимости. Новые правила используются только чтобы переопределить или дополнить соглашения по умолчанию.
В исходном коде для использования атрибутов необходимо подключить пространство имен System.ComponentModel.DataAnnotations.
[Table(string name, Schema = string)]
Атрибут [Table] используется для классов и определяет произвольное имя (name) для создаваемой таблицы. Именованный параметр Schema позволяет указать схему, используемую для её генерации.
[Column(string name, Order = int, TypeName = string)]
Этот атрибут применяется для свойства классов Модели и определяет имя колонки (name), связанной с ним. Кроме того, именованные параметры позволяют:
- Order – задать её порядковый номер в таблице.
- TypeName – уточнить тип, который будет использоваться в таблице для данного свойства.
[Key]
Позволяет отметить одно или несколько свойств, которые будут рассматриваться как первичный ключ для создаваемой таблицы. Атрибут применим только к простым типам.
[Required]
Указывает на то, что данное свойство и соответствующее поле в базе данных обязательно должны содержать значение, то есть не могут быть равны null. Используется только со следующими свойствами классов Модели:
- Простые (скалярные) – в этом случае поле в таблице будет отмечено как not null, даже если сам тип може�� принимать значение null.
- Навигационные, содержащие внешний ключ – указывает на обязательное его наличие.
Атрибут не применим к сложным типам и навигационным свойствам, содержащим коллекции.
[StringLength(int maximumLength, MinimumLength = int)]
Определяет максимальную длину строки в maximumLength символов и используется только для свойств типа string. При этом ядро Entity Framework Code First игнорирует значение MinimumLength, так как в базах данных нет ограничения по минимальной длине строки.
[MaxLength(int length)]
Данный атрибут устанавливает максимальную длину массива (length) и может быть использован только со свойствами типа byte[] или string. Указать максимально возможный размер (max) можно используя константу "–1" в качестве параметра length. При этом другие отрицательные величины не допустимы.
[Timestamp]
Используется для свойств, соответствующая колонка в таблице базы данных которых должна получить тип TimeStamp. Необходимо отметить, что её значение будет уникальным. Однако это не дает гарантии, что оно как-либо будет связано с реальным временем.
Кроме того, данный атрибут может быть применен только к одному свойству каждой сущности (классу Модели). Дело в том, что именно такое ограничение устанавливают большинство современных систем управления базами данных.
[ForeignKey(string name)]
Указывает навигационному свойству связанное с ним свойство под именем name, которое является внешним ключом. Также может использоваться и наоборот – указывать навигационное свойство для свойства, хранящего внешний ключ. Используется в случаях, если имена не починяются соглашениям по умолчанию.
[InverseProperty(string property)]
С помощью этого атрибута можно явно указать связь между навигационными свойствами двух классов Модели. Это может быть необходимо, если их имена не подчиняются соглашениям по умолчанию. Кроме того, один класс Модели может иметь более одной связи с другим.
Для большей ясности рассмотрим использование [ForeignKey] и [InverseProperty] на небольшом примере. Пусть класс Gamer описывает игрока, а класс Team команду. Каждый игрок должен быть в основном составе какой-либо команды, но при этом может является запасным в одной из других. Код классов в этом случае может выглядеть следующим образом:
public class Gamer
{
public int Id { get; set; }
public string Name { get; set; }
public int MainId { get; set; }
public int? ReserveId { get; set; }
[InverseProperty("Gamers")]
[ForeignKey("MainId")]
public virtual Team GamersTeam { get; set; }
[InverseProperty("Reserve")]
[ForeignKey("ReserveId")]
public virtual Team ReserveTeam { get; set; }
}
public class Team
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Gamer> Gamers { get; set; }
public virtual ICollection<Gamer> Reserve { get; set; }
}
В данном случае рассматриваемые атрибуты [ForeignKey] отмечают связи внутри класса. А [InverseProperty] устанавливают взаимосвязь свойств класса Gamer и коллекций класса Team.
[DatabaseGenerated(DatabaseGeneratedOption databaseGeneratedOption)]
Атрибут [DatabaseGenerated] отмечает свойство, для которого значение в связанной с ним колонке таблицы должно вычисляться самой базой данных. Его параметр databaseGeneratedOption может устанавливать один из следующих вариантов генерации значений:
- DatabaseGeneratedOption.Identity – только при добавлении строки (такой вариант, как правило, по умолчанию используется для первичных ключей в таблицах);
- DatabaseGeneratedOption.Computed – при добавлении или обновлении строки;
- DatabaseGeneratedOption.None – генерация значения для данной строки отключена.
[NotMapped]
Атрибут [NotMapped] отмечает свойства, которые должны быть проигнорированы при создании таблиц в базе данных. Это могут быть вспомогательные или вычисляемые свойства.
Модифицируем классы Модели
Для примера давайте модифицируем классы Модели веб-приложения. И начнем с BookDetails:
В первую очередь сменим имя таблицы с BookDetails на Catalog с помощью [Table]. Затем воспользуемся атрибутом [Required], чтобы явно указать требуемые для заполнения поля. Кроме того, у всех свойств типа string установим ограничение по максимальной длине строки, указав соответствующее значение в [StringLength]. Для наглядности в исходном коде выделены несколько первых строк с изменениями.
namespace BookCatalog.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BookCatalog.Resources.Models;
[Table("Catalog")]
public class BookDetails
{
public int Id { get; set; }
[Display(Name = "Title", ResourceType = typeof(BookDetailsRes))]
[Required]
[StringLength(128)]
public string Title { get; set; }
[Display(Name = "Author", ResourceType = typeof(BookDetailsRes))]
[Required]
[StringLength(128)]
public string Author { get; set; }
public int LanguageId { get; set; }
public int? PublisherId { get; set; }
[Display(Name = "PublishedAt", ResourceType = typeof(BookDetailsRes))]
[Required]
public DateTime PublishedAt { get; set; }
[Display(Name = "Url", ResourceType = typeof(BookDetailsRes))]
[Required]
[StringLength(256)]
public string Url { get; set; }
[Display(Name = "Description", ResourceType = typeof(BookDetailsRes))]
[Required]
[StringLength(512)]
public string Description { get; set; }
[Display(Name = "Rating", ResourceType = typeof(BookDetailsRes))]
public int? Rating { get; set; }
/// <summary>Gets or sets a value indicating whether
/// the book is free (true) or not (false).</summary>
[Display(Name = "IsFree", ResourceType = typeof(BookDetailsRes))]
[Required]
public bool IsFree { get; set; }
/// <summary>Gets or sets a value indicating whether
/// the book is visible in the catalog (true) or not (false).</summary>
[Display(Name = "IsVisible", ResourceType = typeof(BookDetailsRes))]
[Required]
public bool IsVisible { get; set; }
[Display(Name = "Tags", ResourceType = typeof(BookDetailsRes))]
public virtual ICollection<Tag> Tags { get; set; }
[Display(Name = "Language", ResourceType = typeof(BookDetailsRes))]
public virtual Language Language { get; set; }
[Display(Name = "Publisher", ResourceType = typeof(BookDetailsRes))]
public virtual Publisher Publisher { get; set; }
}
}
Аналогичные модификации произведем и в остальных классах Модели:
namespace BookCatalog.Models
{
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BookCatalog.Resources.Models;
public class Language
{
public int Id { get; set; }
[Display(Name = "Name", ResourceType = typeof(LanguageRes))]
[Required]
[StringLength(64)]
public string Name { get; set; }
[Display(Name = "Books", ResourceType = typeof(LanguageRes))]
public virtual ICollection<BookDetails> Books { get; set; }
}
}
namespace BookCatalog.Models
{
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BookCatalog.Resources.Models;
public class Publisher
{
public int Id { get; set; }
[Display(Name = "Title", ResourceType = typeof(PublisherRes))]
[Required]
[StringLength(128)]
public string Title { get; set; }
[Display(Name = "Homepage", ResourceType = typeof(PublisherRes))]
[StringLength(256)]
public string Homepage { get; set; }
[Display(Name = "Books", ResourceType = typeof(PublisherRes))]
public virtual ICollection<BookDetails> Books { get; set; }
}
}
namespace BookCatalog.Models
{
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BookCatalog.Resources.Models;
public class Tag
{
public int Id { get; set; }
[Display(Name = "Text", ResourceType = typeof(TagRes))]
[Required]
[StringLength(32)]
public string Text { get; set; }
[Display(Name = "Books", ResourceType = typeof(TagRes))]
public virtual ICollection<BookDetails> Books { get; set; }
}
}
Обратите внимание, что как уже было отмечено, атрибуты используются для дополнения соглашений по умолчанию. Например, нет необходимости указывать [Key] у свойств Id или с помощью [Column] определять имена и порядок колонок. В этих случаях отлично подходят варианты по умолчанию.
Скомпилируем создаваемый проект. Поскольку Модель была изменена, то перед его запуском необходимо удалить файл с базой данных, расположенный в папке App_Data. Сделать это можно непосредственно из Solution Navigator / Solution Explorer.
Теперь запустим демонстрационное веб-приложение. Если после его работы посмотреть структуру базы данных, то можно отметить, что атрибуты [Required] трансформировались в указания not null для колонок. Для строк определена максимальная длинна, исходя из значений [StringLength]. А имя таблицы с данными о книгах теперь Catalog. Стоит отметить, что это не сказалось на имени вспомогательной таблицы. Она по прежнему называется BookDetailsTags.
Используя приведенные выше атрибуты можно переопределять и дополнять соглашения по умолчанию. Однако данный подход не всегда уместен. В частности:
- Атрибуты не покрывают все ситуации и иногда не очень наглядны.
- Не всегда желательно, и даже возможно, изменять код Модели.
- Атрибуты Data Annotation используются и в других библиотеках. А значит их применение может оказать воздействие на другие части кода приложения.
В подобных ситуациях необходимо использование Fluent API, который рассмотрим в следующей части.
Исходный код проекта (C#, Visual Studio 2010): mvc3-in-depth-ef-04.zip