Настройка столбцов таблицы

140

При рассмотрении подхода Code-First в статье “Использование Code-First” вы увидели некоторые соглашения по конфигурации, которые использует этот подход. В частности вы видели, как настроить сущностную модель с помощью аннотаций данных, выраженных через атрибуты C#, и с помощью Fluent API. В этой и последующих статьях мы более подробно рассмотрим эти способы настройки модели данных.

Настройка типов столбцов

Ранее, при рассмотрении примера использования Code-First, вы уже видели использование некоторых атрибутов метаданных, которые позволяли настраивать тип данных столбца в таблице базы данных и применять к нему ограничения (например, указав поддерживает ли он NULL-значения). Далее мы рассмотрим эти атрибуты более подробно.

Ограничение длины

В таблице ниже показаны соглашения об ограничении длины столбцов, их реализация в виде аннотаций и в Fluent API:

Соглашения Entity Framework по ограничению длины столбцов
Соглашение

В настройках модели можно указывать ограничение на максимальную длину для свойств, имеющих тип String или byte[], которые отображаются в таблице на соответствующие типы NVARCHAR и VARBINARY. По умолчанию Code-First задает для них максимальную длину NVARCHAR(max) и VARBINARY(max).

Атрибуты метаданных MinLength (n)
MaxLength (n)
StringLength (n, MinimumLength=m)
Реализация в Fluent API
Entity<T>().Property(t => t.PropertyName).HasMaxLength(n)

Ограничение длины можно наложить на строки или массивы байт. Согласно соглашениям, Code-First использует только ограничение максимальной длины, это означает, что SQL Server устанавливает тип данных для строк и массивов байт как NVARCHAR(n) и VARBINARY(n), где n – это длина, указанная в ограничении. По умолчанию, если к свойствам модели не использовалось ограничение по длине, Code-First установит максимально возможную длину столбцов – NVARCHAR(max) и VARBINARY(max).

Как показано в таблице, используя аннотации данных вы можете настроить минимальную длину для свойства модели с помощью атрибута MinLength – это ограничение никак не повлияет на таблицу базы данных. (Как описывалось ранее, атрибуты метаданных могут использоваться не только в Entity Framework, а, например, также в проверки достоверности модели ASP.NET.) Именно поэтому в Fluent API отсутствует метод HasMinLength(), т.к. этот API-интерфейс является частью Entity Framework и отвечает за настройку соглашений, связанных только с Code-First.

Стоит обратить внимание, что указать максимальную и минимальную длину поля можно в одном атрибуте StringLength, используя именованные параметры этого атрибута. В следующем примере показано использование ограничения по длине с помощью аннотаций (здесь мы используем пример модели, который создали в статье “Использование Code-First” ранее):

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

namespace CodeFirst
{
    public class Customer
    {
        public int CustomerId { get; set; }

        [MaxLength(30)]
        public string FirstName { get; set; }

        public string LastName { get; set; }

        [MinLength(6)]
        [MaxLength(100)]
        public string Email { get; set; }

        public int Age { get; set; }
        public byte[] Photo { get; set; }

        // Ссылка на заказы
        public virtual List<Order> Orders { get; set; }
    }

    public class Order
    {
        public int OrderId { get; set; }

        [StringLength(500, MinimumLength=5)]
        public string ProductName { get; set; }

        public string Description { get; set; }
        public int Quantity { get; set; }
        public DateTime PurchaseDate { get; set; }

        // Ссылка на покупателя
        public Customer Customer { get; set; }
    }
}

А в следующем коде аналогичная настройка производится с помощью Fluent API (напомню, что для использования этого API-интерфейса необходимо переопределить метод настройки конфигурации OnModelCreating() в классе контекста, которым в нашем примере является класс SampleContext):

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().Property(c => c.FirstName)
        .HasMaxLength(30);

    modelBuilder.Entity<Customer>().Property(c => c.Email)
        .HasMaxLength(100);

    modelBuilder.Entity<Order>().Property(o => o.ProductName)
        .HasMaxLength(500);
}

Явное указание типа столбца

Соглашения Entity Framework по указанию типа столбца
Соглашение

Тип данных столбца в базе по умолчанию определяется поставщиком базы данных, которую вы используете. В SQL Server тип String в свойстве модели отражается на тип NVARCHAR в таблице базы данных, тип byte[] отражается на тип VARBINARY и т.д. В настройках модели можно явно указывать тип данных, который будет использоваться в таблице.

Атрибут метаданных Column (TypeName="тип данных")
Реализация в Fluent API
Entity<T>().Property(t => t.PropertyName).HasColumnType("тип данных")

Как описывалось ранее, Entity Framework автоматически отображает типы данных модели на SQL-совместимые типы данных. Code-First позволяет управлять этим процессом, для того чтобы явно указать тип данных для столбца, как показано в примере ниже:

public class Customer
{
    [Column(TypeName="smallint")]
    public int CustomerId { get; set; }

    // ...

    [Column(TypeName = "image")]
    public byte[] Photo { get; set; }

    // ...
}


// то же самое с помощью Fluent API

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().Property(c => c.CustomerId)
        .HasColumnType("smallint");

    modelBuilder.Entity<Customer>().Property(c => c.Photo)
        .HasColumnType("image");
}

Поддержка NULL-значений для столбца

Соглашения Entity Framework по указанию поддержки значений NULL
Соглашение

Entity Framework автоматически указывает для типов string и byte[] поддержку NULL в таблице базы данных, а для типов значений (int, long, …), DateTime, char, bool поддержку NOT NULL.

Атрибут метаданных Required
Реализация в Fluent API
Entity<T>().Property(t => t.PropertyName).IsRequired()

Соглашение Entity Framework о поддержке значений NULL в столбце таблицы гласит о том, что все типы .NET, поддерживающие значение null (объекты), отображаются на SQL-типы с явным указанием инструкции NULL, и наоборот, типы .NET, не поддерживающие значение null (структуры) отображаются на SQL-типы с явным указанием инструкции NOT NULL.

Для того, чтобы явно указать, что тип данных не должен поддерживать значения NULL, нужно использовать атрибут Required в модели данных или использовать метод IsRequired() объекта конфигурации в Fluent API. Для того, чтобы наоборот явно указать, что тип данных должен поддерживать значения NULL, нужно использовать коллекцию Nullable<T> или использовать синтаксис C#, в котором для типов значений, поддерживающих null, указывается знак вопроса после указания этого типа (например, int?).

В примере ниже показано использование этих настроек, для конфигурирования информации об обнуляемых или не обнуляемых типах в таблице базы данных:

public class Customer
{
    public int CustomerId { get; set; }

    [Required]
    public string FirstName { get; set; }
    [Required]
    public string LastName { get; set; }
    
    // Это поле может иметь значение NULL,
    // т.к. мы явно указали тип int?
    public int? Age { get; set; }

    // Аналогично предыдущему свойству
    public Nullable<int> Age1 { get; set; }
    
    // ...
}


// то же самое с помощью Fluent API

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().Property(c => c.FirstName)
       .IsRequired();

    modelBuilder.Entity<Customer>().Property(c => c.LastName)
       .IsRequired();
}

Обратите внимание, что в Fluent API можно настроить только поддержку NOT NULL для ссылочных типов данных, и нельзя настроить поддержку NULL для типов значений, т.к. поддержка NULL для них указывается явно, при объявлении типа свойства в классе модели.

Установка первичных ключей

Соглашения Entity Framework по указанию первичных ключей
Соглашение

Entity Framework ищет свойства модели имеющие имя Id или состоящие из строки “[TypeName] + Id”, например CustomerId для нашего примера и автоматически устанавливает их как первичные ключи в таблице базы данных.

Атрибут метаданных Key
Реализация в Fluent API
Entity<T>().HasKey(t => t.PropertyName)

Entity Framework требует, чтобы каждый класс сущностной модели имел уникальный ключ (т.к. для каждой таблицы в реляционной базе данных должен использоваться первичный ключ). Этот ключ используется в объекте контекста, для отслеживания изменений в объектах модели. Code-First делает некоторые предположения, при поиске ключа в таблице. Например, когда мы сгенерировали базу данных для классов сущностей Customer и Order ранее, при рассмотрении подхода Code-First, то Entity Framework пометил поля CustomerId и OrderId в таблицах, как первичные ключи и задал для них поддержку не обнуляемых типов:

Автоматически сгенерированные первичные ключи для таблиц

Также EF автоматически добавил поддержку автоинкремента в эти поля (напомню, что в T-SQL это делается с помощью инструкции IDENTITY). Чаще всего, первичные ключи в базе данных имеют тип INT или GUID, хотя любой примитивный тип может быть использован в качестве первичного ключа. Первичный ключ в базе данных может состоять из нескольких столбцов таблицы, аналогично, ключ сущностной модели EF может быть составлен из нескольких свойств модели. Позже вы увидите как настроить составные ключи.

Явная установка первичных ключей

В случае двух наших классов Customer и Order беспокоиться об явном указании первичного ключа не стоит, т.к. мы используем свойства CustomerId и OrderId, которые соответствуют соглашению об именовании ключей - “[TypeName] + Id”. Давайте рассмотрим пример, в котором нужно явно указать первичный ключ. Добавьте в файл модели следующий простой класс:

public class Project
{
    public Guid Identifier { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public decimal Cost { get; set; }
}

Нашей целью является указать, что свойство Identifier в этом классе является первичным ключом таблицы. Как видите имя этого свойства не соответствует соглашению Entity Framework об именовании первичных ключей.

Добавьте в класс контекста SampleContext описание новой таблицы:

public class SampleContext : DbContext
{
    // ...
    public DbSet<Project> Projects { get; set; }
        
    // ...
}

Теперь, если вы запустите наше приложение и вставите данные нового заказчика, то в классе DbModelBuilder будет сгенерировано исключение, в связи с тем, что он не может корректно построить сущностную модель данных. (Напомню, что наше приложение является простым сайтом ASP.NET в котором реализована возможность вставить нового заказчика.)

Ошибка Entity Framework при поиске первичного ключа в классе модели

Это исключение вызвано тем, что Entity Framework не нашел в таблице Project свойство с именем Id или ProjectId, и Code-First не может понять, какое поле нужно использовать в качестве первичного ключа таблицы. Эту проблему можно исправить, используя аннотации данных или Fluent API, как показано в примере ниже:

public class Project
{
    [Key]
    public Guid Identifier { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public decimal Cost { get; set; }
}


// то же самое с помощью Fluent API

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Project>().HasKey(p => p.Identifier);
}

Обратите внимание, что при использовании Fluent API метод HasKey() указывается после вызова метода Entity<T>(), а не после вызова Entity<T>().Property(), как это делалось в примерах выше, т.к. первичный ключ устанавливается на уровне таблицы, а не на уровне свойств.

Настройка автоинкремента для первичных ключей

Соглашения Entity Framework по указанию автоинкремента для первичных ключей
Соглашение

Если свойство первичного ключа в модели имеет тип int, для него автоматически устанавливается счетчик в базе данных с помощью инструкции IDENTITY (1,1).

Атрибут метаданных DatabaseGenerated (DatabaseGeneratedOption)
Реализация в Fluent API
Entity<T>().Property(t => t.PropertyName)
.HasDatabaseGeneratedOption (DatabaseGeneratedOption)

Как видно из таблицы, следуя соглашениям, Entity Framework указывает автоинкремент для свойств имеющих тип int. В созданной ранее таблице Project для первичного ключа указывается тип Guid, вследствие чего, EF не использует счетчик для этого поля, при создании таблицы в базе данных. Это показано на рисунке:

Отключение автоинкремента для столбца типа Guid

Давайте добавим в наш проект новую веб-форму, которую назовем DatabaseGenerated.aspx. В обработчике Page_Load добавьте следующий код, в котором мы добавляем новые данные в таблицу Project. В данном случае эти данные будут добавляться всякий раз, когда мы открываем страницу нашей веб-формы в браузере.

using System;
using System.Data.Entity;
using CodeFirst;

namespace ProfessorWeb.EntityFramework
{
    public partial class DatabaseGenerated : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            // Эта настройка нужна для того, чтобы база данных автоматически
            // удалялась и заново создавалась при изменении структуры модели
            // (чтобы было удобно тестировать примеры)
            Database.SetInitializer(
                new DropCreateDatabaseIfModelChanges<SampleContext>());

            SampleContext context = new SampleContext();

            Project project = new Project
            {
                StartDate = DateTime.Now,
                EndDate = DateTime.Now.AddMonths(1),
                Cost = 8000M
            };

            context.Projects.Add(project);
            context.SaveChanges();
        }
    }
}

Запустите проект и откройте веб-форму DatabaseGenerated.aspx. В результате в таблицу Project добавится новая запись:

Идентификатор Guid без автоинкремента

Ни базе данных, ни Entity Framework не известно, что мы хотели бы создавать новый идентификатор Guid для каждой новой записи, поэтому будет автоматически создан идентификатор, содержащий одни нули. Если вы обновите страницу в браузере (фактически в данном случае код попытается вставить новую запись в таблицу), то Entity Framework вернет исключение SqlException, которое возникает из-за того, что мы пытаемся вставить запись имеющую идентификатор, который уже существует в таблице, т.е. в данном случае срабатывает ограничение первичного ключа – он должен быть уникальным для каждой новой записи.

В результате, чтобы решить данную проблему, нам бы потребовалось генерировать уникальный идентификатор Guid в коде. Таблицы, использующие автоинкремент для первичных ключей лишены такой дополнительной работы, т.к. для каждой новой вставляемой записи, счетчик создает новое значение для первичного ключа, извлекая значение первичного ключа для последней записи и прибавляя к нему 1 (если использовалась конструкция IDENTITY(1,1)).

Чтобы решить эту проблему для ключей, имеющих тип отличный от int, нужно использовать атрибут метаданных DatabaseGenerated, в конструкторе которого указывается перечисление DatabaseGeneratedOption, имеющее три возможных значения:

None

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

Identity

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

Computed

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

Измените класс модели таким образом, чтобы указать базе данных возможность создавать уникальный первичный ключ:

public class Project
{
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Identifier { get; set; }
        public DateTime StartDate { get; set; }
        public DateTime EndDate { get; set; }
        public decimal Cost { get; set; }
}

Запустите пример проекта и несколько раз обновите страницу, чтобы вставить несколько записей в таблицу и убедиться, что исключение больше не генерируется. На рисунке ниже показаны данные, добавленные в таблицу:

Добавленные записи при использовании автоинкремента для столбца типа Guid

Обратите внимание на автоматически сгенерированные идентификаторы для проектов. Того же эффекта можно добиться, используя метод HasDatabaseGeneratedOption() в Fluent API:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Project>().Property(p => p.Identifier)
        .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}

Работа со сложными типами данных

Entity Framework поддерживает возможность использования сложных типов, начиная с первой версии. Фактически сложный тип в .NET является классом, ссылку на который можно использовать в классе модели. Сложный тип не имеет ключа и может использоваться в нескольких объектах модели. Давайте рассмотрим пример следующей модели:

public class User
{
    public int UserId { get; set; }
    public int SocialNumber { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string StreetAddress { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}

В этом классе адрес проживания пользователя можно выделить в отдельный класс и сослаться на него:

public class User
{
    public int UserId { get; set; }
    public int SocialNumber { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int AddressId { get; set; }
    public string StreetAddress { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}

По соглашениями Entity Framework разберет эту модель – как две отдельные таблицы. Но нашей целью является создание сложного типа из класса Address. Традиционный способ для создания сложного типа из класса Address представляет удаление идентификатора AddressId:

public class Address
{
    // public int AddressId { get; set; }
    public string StreetAddress { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}

В дополнение к правилу, что сложный тип не должен иметь ключ, Code-First накладывает два других правила, которые должны быть выполнены для обнаружения сложного типа. Во-первых, сложный тип должен содержать только простые свойства. Во-вторых, в классе, который использует этот тип, не разрешается указывать тип коллекции для свойства сложного типа. Другими словами, если вы хотите использовать сложный тип Address в классе User, то свойство, имеющее этот тип, не должно быть помечено как List<Address> или использовать другую коллекцию.

Как показано на рисунке ниже, после запуска приложения Code-First распознает сложный тип и создает специальные поля в таблице User (не забудьте добавить объявление User в классе контекста):

Столбцы сложного типа в таблице

Обратите внимание, как поля, описывающие адрес пользователя, названы: ИмяСложногоТипа_ИмяСвойства. Это является соглашением Entity Framework по именованию сложных типов.

Настройка сложных типов в обход соглашениям Code-First

Что делать, если ваш класс, описывающий сложный тип, не следует соглашениям Entity Framework, например, вы хотите использовать в классе Address поле AddressId? Если сейчас мы добавим это поле в класс Address и запустим проект, то вместо одной таблицы User и сложного типа Address, Entity Framework создаст две таблицы, связанные друг с другом внешним ключом. Чтобы исправить эту проблему, вы можете явно указать атрибут ComplexType в классе модели или использовать метод ComplexType() класса DbModelBuilder в Fluent API:

[ComplexType]
public class Address
{
    public int AddressId { get; set; }
    public string StreetAddress { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}


// то же самое с помощью Fluent API

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.ComplexType<Address>();
}

Выше было сказано, что класс, описывающий сложный тип, должен иметь только простые свойства (т.е. не ссылающиеся на другие объекты). Это соглашение можно преодолеть, используя все те же средства. Ниже показан пример, в котором был добавлен новый сложный тип UserInfo, ссылающийся на другой тип FullName:

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

[ComplexType]
public class UserInfo
{
    public int SocialNumber { get; set; }

    // Не является простым свойством
    public FullName FullName { get; set; }
}

public class FullName
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

[ComplexType]
public class Address
{
    public int AddressId { get; set; }
    public string StreetAddress { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}

Благодаря указанию Code-First на то, что UserInfo является сложным типом с помощью атрибута ComplexType, мы преодолели ограничение, накладываемое на сложные типы при использовании соглашения по умолчанию.

Стоит отметить, что Code-First позволяет настраивать сложные типы, также, как и обычные таблицы с использованием Fluent API или аннотаций. Ниже показан пример, для настройки сложного типа Address:

[ComplexType]
public class Address
{
    public int AddressId { get; set; }

    [MaxLength(100)]
    public string StreetAddress { get; set; }
    public string City { get; set; }
    public string ZipCode { get; set; }
}


// то же самое с помощью Fluent API

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.ComplexType<Address>().Property(a => a.StreetAddress)
        .HasMaxLength(100);
}

На рисунке ниже показана структура таблицы User. Здесь вы можете увидеть, как EF именует свойства сложных типов с ссылками внутри и то, что EF накладывает ограничение на поле StreetAddress:

Использование ограничений для сложных типов

Описание других настроек

В этом разделе мы кратко рассмотрим все оставшиеся настройки столбцов таблицы, которые используются довольно редко в силу своих специфических особенностей.

Столбцы типа Timestamp

Тип данных TIMESTAMP в T-SQL указывает столбец, определяемый как VARBINARY(8) или BINARY(8), в зависимости от свойства столбца принимать значения NULL. Для каждой базы данных система содержит счетчик, значение которого увеличивается всякий раз, когда вставляется или обновляется любая строка, содержащая ячейку типа TIMESTAMP, и присваивает этой ячейке данное значение. Таким образом, с помощью ячеек типа TIMESTAMP можно определить относительное время последнего изменения соответствующих строк таблицы. (ROWVERSION является синонимом TIMESTAMP.)

Само по себе значение, сохраняемое в столбце типа TIMESTAMP, не представляет никакой важности. Этот столбец обычно используется для определения, изменилась ли определенная строка таблицы со времени последнего обращения к ней. Это позволяет решать вопросы параллельного доступа к таблице базы данных, позволяя блокировать другие потоки, если текущий поток изменил значения в строке.

В Code-First для указания на то, что столбец должен иметь тип TIMESTAMP должен использоваться одноименный атрибут Timestamp в аннотациях или метод IsRowVersion() в Fluent API, как показано в примере ниже:

[Timestamp]
public byte[] RowVersion { get; set; }


// то же самое с помощью Fluent API

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Project>().Property(p => p.RowVersion)
        .IsRowVersion();
}

Менее распространенным способом, для обеспечения безопасности при работе с параллельными потоками, является указание проверки параллелизма для каждого столбца. Такой способ может еще использоваться в СУБД, не поддерживающих типы Timestamp/Rowversion. При работе таким образом, поток не проверяет, изменилась ли запись в таблице, а просто блокирует доступ к ней для других потоков, пока сам не завершит процесс записи. Для указания столбцов, которые должны пройти проверку на параллелизм, используется атрибут ConcurrencyCheck в аннотациях, либо метод IsConcurrencyToken() в Fluent API.

Изменение кодировки строк с Unicode на ASCII

По умолчанию Entity Framework преобразует все строковые типы данных модели, такие как string или char, в строковые типы данных SQL, использующие двухбайтовую кодировку Unicode – NVARCHAR или NCHAR. Вы можете изменить это поведение, и явно указать EF использовать однобайтовую кодировку ASCII – соответственно будут использоваться типы VARCHAR и CHAR. Для этого нужно использовать метод IsUnicode() с переданным ему логическим параметром false в Fluent API. Аннотации не предоставляют возможность настройки кодировки строк.

Настоятельно не рекомендую вам использовать эту настройку, если планируется хранить строковую информацию в базе данных в символах, не совместимых с ASCII (например, русские символы).

Указание точности для типа Decimal

Для указания точности для типа Decimal (количество цифр в числе) и масштаба (количество цифр справа от десятичной точки в числе) можно использовать метод HasPrecision() с передачей ему двух параметров, который используется в Fluent API. Аннотации данных в Code-First не предлагают альтернативы этому методу. По умолчанию Entity Framework устанавливает точность 18, а масштаб 2 для типов Decimal.

В примере ниже показано использование этого метода, для свойства Cost таблицы Project, которую мы создали ранее, при рассмотрении первичных ключей:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Project>().Property(p => p.Cost)
        .HasPrecision(6, 3);
}
Пройди тесты
Лучший чат для C# программистов