Entity Framework. Часть 4 – Атрибуты в Code First

В прошлой части были рассмотрены соглашения, на основе которых Entity Framework Code First генерирует таблицы для хранения данных. Но что делать, если требования бизнес-логики не совпадают с соглашениями по умолчанию? Можно ли сделать свойство MySupertProperty первичным ключом?

Для таких ситуация возможно два типа решений:

  1. Использование атрибутов, указываемых для классов Модели и их свойств.
  2. Описание параметров и соотношений с использованием вызовов 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.cs

В первую очередь сменим имя таблицы с 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; }
    }
}

Аналогичные модификации произведем и в остальных классах Модели:

  • Language.cs
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; }
    }
}
  • Publisher.cs
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; }
    }
}
  • Tag.cs
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

Комментарии (17) -

Ринат 21.08.2011 1:17:24

немного не понял куда ссылается [InverseProperty("ReservePersons")]

Там ошибка была. Поправил.

Код Модели при этом остается не именным. => неизменным

@ Michal: Спасибо, исправил.

Евгений 19.11.2011 4:16:19

Андрей, еще раз спасибо! Связал таблицы благодаря Вашей статье.

Евгений 20.11.2011 2:54:04

Андрей, добрый день!
Скажите пожалуйста, как будут вести себя таблицы, объявленные с помощью Code First, при каскадном обновлении/удалении данных? Обязательно ли в SQL Server, в котором находится БД, создавать схему данных, либо же данные будут каскадно обновляться посредством Code First без создания схемы?
Заранее спасибо!

@ Евгений: По умолчанию EF Code First устанавливается каскадное удаление. Изменить поведение можно с помощью FluentAPI (описание и примеры есть в моем блоге тут andrey.moveax.ru/.../ )

Артём 25.07.2012 1:12:09

Добрый вечер(Ночь). Разработку на asp.net mvc начал изучать совсем недавно, и столкнулся с проблемой. База данныех была написана уже поэтому было решено использовать метод База Сначала.

В строго тепизированном представлении , основаном на сегенерированом классе из EF(Допустим Book), Как поменять или дописать атрибуты. Допустим если я хочу применить валидацию.
Влоб добавляя в файл edmx не фига не выходит со всременем Они стираются автоматом. как быть в такой ситуации.?
      

Артём 25.07.2012 17:13:14

Точнее вопрос вот наверное в чем. Можно ли к существующий базе, сделать свою модель и замапить ее как в codeFirst только без создания базы.?

@ Артём: Я не часто сталкивался с Database First поэтому могу ошибиться, но добавить атрибуты на созданные сущности не получится. Можно попробовать навесить их после (провайдерами), но в данном случае не очень удачное решение.

А вот второе можно. В EF Power Tools есть "Reserve Engineer Code First" (в контекстном меню проекта). С его помощью можно из готовой базы получить набор классов для Code First.

visualstudiogallery.msdn.microsoft.com/.../

@ Артём: валидацию можно навесить с помощью partial классов. Тогда она останется при изменении edmx

Дамир Гарипов 15.11.2012 16:57:13

Добрый день Андрей!

Раньше я работал с данными с помощью ADO.NET (DataSet, DataTable и т.д.). У меня было приложение, которое постранично извлекало из БД и передавало данные на веб-страницу (в формате JSON) вместе с данными схемы (названия столбцов, количество строк, номер страницы и т.д.). Сейчас я переношу это приложение под паттерн MVC с Entity Framework. Такой вопрос: есть какой-либо способ получить коллекцию свойств определенного класса или сущности? Или же придется создавать коллекцию самому?

Несколько не понял что требуется.

Если речь про подключение к произвольной БД в run-time, то в EF с таким не сталкивался. На этапе разработке можно сгенерировать сущности по готовый БД используя подход Database First.

Если речь о том, что надо получить строки из БД (их число), то есть соответствующие методы Linq 2 Entities. Например, context.MyDbSet.Count() - число записей в MyDbSet и т.д.

Дамир Гарипов 16.11.2012 14:44:39

@ Andrey:

Дамир Гарипов :
есть какой-либо способ получить коллекцию свойств определенного класса или сущности? Или же придется создавать коллекцию самому?

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

Дамир Гарипов 16.11.2012 14:52:06

Вчера нашел способ:
MetadataWorkspace workspace = ((IObjectContextAdapter)db).ObjectContext.MetadataWorkspace;
EntityContainer properties = workspace.GetEntityContainer("myEntities", true, DataSpace.CSpace);

foreach (var item in ((EntityType)properties.BaseEntitySets["table"].ElementType).Properties)
{
Console.WriteLine("Type: {0}, MetaDataPropertyName in Model: {1} ",
                      item.TypeUsage.EdmType.Name, item.Name);
}

Пытаюсь переименовать колонку в одной из своих таблиц.
Указываю аттрибут [Column("GroupCode")] для свойства Code, генерируется код:
public partial class Rename : DbMigration
    {
        public override void Up()
        {
            RenameColumn(table: "dbo.PromoCodes", name: "Code", newName: "GroupCode");
        }
        
        public override void Down()
        {
            RenameColumn(table: "dbo.PromoCodes", name: "GroupCode", newName: "Code");
        }
    }

Но при применение миграции каждый раз получаю ошибку:
Fatal error encountered during command execution. ---> MySql.Data.MySqlClient.MySqlException (0x80004005): Parameter '@columnType' must be defined.

Есть идеи как это обойти?

Ilya6820 Как я помню у RenameColumn есть параметер с аргументами. Попробовать передать columnType через него?

Добавить комментарий