Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Замечание
EF6 и более поздние версии — функции, API и т. д., рассмотренные на этой странице, были представлены в Entity Framework 6. Если вы используете более раннюю версию, некоторые или все сведения не применяются.
При использовании Code First ваша модель вычисляется из классов с помощью набора соглашений. Соглашения Code First по умолчанию определяют такие вещи, как какое свойство становится первичным ключом сущности, имя таблицы, с которой сопоставляется сущность, и какая точность и масштаб заданы для десятичных столбцов по умолчанию.
Иногда эти соглашения по умолчанию не идеально подходят для модели, и их необходимо обойти, настроив множество отдельных сущностей с помощью заметок данных или API Fluent. Пользовательские соглашения Code First позволяют определять свои собственные соглашения, предоставляющие настройки по умолчанию для модели. В этом пошаговом руководстве мы рассмотрим различные типы пользовательских соглашений и способы создания каждого из них.
Соглашения на основе моделей
На этой странице рассматривается API DbModelBuilder для пользовательских соглашений. Этот API должен быть достаточным для разработки большинства пользовательских соглашений. Однако существует также возможность создавать соглашения на основе моделей - соглашения, которые управляют окончательной моделью после ее создания - для обработки расширенных сценариев. Дополнительные сведения см. в модельно-ориентированных конвенциях.
Наша модель
Начнем с определения простой модели, которую мы можем использовать с нашими соглашениями. Добавьте в проект следующие классы.
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
}
public class Product
{
public int Key { get; set; }
public string Name { get; set; }
public decimal? Price { get; set; }
public DateTime? ReleaseDate { get; set; }
public ProductCategory Category { get; set; }
}
public class ProductCategory
{
public int Key { get; set; }
public string Name { get; set; }
public List<Product> Products { get; set; }
}
Знакомство с пользовательскими соглашениями
Давайте напишем соглашение, которое настраивает любое свойство с именем Key в качестве первичного ключа для его типа сущности.
Соглашения включены в построителе моделей, к которому можно получить доступ, переопределив OnModelCreating в контексте. Обновите класс ProductContext следующим образом:
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
}
}
Теперь любое свойство в нашей модели с именем Key будет настроено как первичный ключ той сущности, частью которой оно является.
Мы также можем сделать наши соглашения более конкретными, отфильтровав тип свойства, которое мы собираемся настроить:
modelBuilder.Properties<int>()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
Это настроит все свойства с именем Key так, чтобы они были первичным ключом для своей сущности, но только в том случае, если они являются целыми числами.
Интересная особенность метода IsKey заключается в том, что он является аддитивным. Это означает, что при вызове IsKey для нескольких свойств все они станут частью составного ключа. Одним из предостережением этого является то, что при указании нескольких свойств ключа необходимо также указать порядок для этих свойств. Это можно сделать, вызвав метод HasColumnOrder, как показано ниже:
modelBuilder.Properties<int>()
.Where(x => x.Name == "Key")
.Configure(x => x.IsKey().HasColumnOrder(1));
modelBuilder.Properties()
.Where(x => x.Name == "Name")
.Configure(x => x.IsKey().HasColumnOrder(2));
Этот код настраивает типы в нашей модели, чтобы иметь составной ключ, состоящий из столбца int Key и столбца string Name. Если мы рассмотрим модель в конструкторе, она будет выглядеть следующим образом:
Еще одним примером соглашений о свойствах является настройка всех свойств DateTime в моей модели для сопоставления с типом datetime2 в SQL Server вместо datetime. Это можно сделать следующим образом:
modelBuilder.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
Классы соглашений
Другой способ определения соглашений — использовать класс соглашения для инкапсулирования соглашения. При использовании класса Convention создается тип, наследующий от этого класса в пространстве имен System.Data.Entity.ModelConfiguration.Conventions.
Мы можем создать класс соглашения с соглашением datetime2, которое мы показали ранее, выполнив следующие действия:
public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
}
}
Чтобы указать EF использовать это соглашение, вы добавите его в коллекцию соглашений в OnModelCreating, которая, если вы следуете инструкции, выглядит следующим образом.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<int>()
.Where(p => p.Name.EndsWith("Key"))
.Configure(p => p.IsKey());
modelBuilder.Conventions.Add(new DateTime2Convention());
}
Как видно, мы добавим экземпляр нашего соглашения в коллекцию соглашений. Наследование от Конвенции обеспечивает удобный способ группирования и совместного использования соглашений между командами или проектами. Например, можно использовать библиотеку классов с общим набором соглашений, используемых всеми проектами организаций.
Настраиваемые атрибуты
Другим важным применением соглашений является возможность использования новых атрибутов при конфигурации модели. Чтобы проиллюстрировать это, давайте создадим атрибут, который можно использовать для пометки свойств String как не в Юникоде.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NonUnicode : Attribute
{
}
Теперь создадим соглашение, чтобы применить этот атрибут к нашей модели:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
.Configure(c => c.IsUnicode(false));
С помощью этого соглашения можно добавить атрибут NonUnicode к любому из наших строковых свойств, что означает, что столбец в базе данных будет храниться как varchar вместо nvarchar.
Одно из замечаний об этом соглашении заключается в том, что если атрибут NonUnicode помещается на что-либо, отличное от строкового свойства, это приведет к возникновению исключения. Это происходит, так как вы не можете настроить IsUnicode для любого типа, отличного от строки. Если это произойдет, вы можете сделать соглашение более конкретным, так чтобы оно отфильтровывало все, что не является строкой.
Хотя приведенное выше соглашение работает для определения пользовательских атрибутов, существует другой API, который может быть гораздо проще использовать, особенно если требуется использовать свойства из класса атрибутов.
В этом примере мы обновим атрибут и изменим его на атрибут IsUnicode, поэтому выглядит следующим образом:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
internal class IsUnicode : Attribute
{
public bool Unicode { get; set; }
public IsUnicode(bool isUnicode)
{
Unicode = isUnicode;
}
}
После этого мы можем задать булево значение для нашего атрибута, чтобы сообщить соглашению, следует ли свойству быть представлено в формате Юникод. Это можно сделать в соглашении, которое у нас уже есть, получив доступ к ClrProperty класса конфигурации следующим образом:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
.Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
Это достаточно просто, но существует более краткий способ достижения этого с помощью метода Having API для соглашений. Метод Having имеет параметр типа Func<PropertyInfo, T>, который принимает PropertyInfo так же, как метод Where, но ожидается, что он вернет объект. Если возвращаемый объект равен NULL, то свойство не будет настроено, что означает, что вы можете использовать его для фильтрации свойств, как в Where, но отличие в том, что он также обрабатывает возвращаемый объект и передает его в метод Configure. Это работает следующим образом:
modelBuilder.Properties()
.Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
.Configure((config, att) => config.IsUnicode(att.Unicode));
Пользовательские атрибуты не являются единственной причиной для использования метода "Having". Он полезен в любом месте, где необходимо учитывать что-либо, что фильтруется при конфигурации типов данных или свойств объектов.
Настройка типов
До сих пор все наши соглашения были для свойств, но существует еще одна область API соглашений для настройки типов в модели. Опыт использования аналогичен тем конвенциям, которые мы видели до сих пор, но параметры настройки будут располагаться на уровне сущности, а не на уровне свойств.
Одна из вещей, которые могут быть полезными для соглашения о уровне типов, заключается в изменении соглашения об именовании таблиц либо для сопоставления с существующей схемой, которая отличается от EF по умолчанию, либо для создания новой базы данных с другим соглашением об именовании. Для этого сначала требуется метод, который может принимать TypeInfo для типа в нашей модели и возвращать имя таблицы для этого типа:
private string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
Этот метод принимает тип и возвращает строку, которая использует нижний регистр с подчеркиванием вместо CamelCase. В нашей модели это означает, что класс ProductCategory будет сопоставлен с таблицей с именем product_category вместо productCategories.
Получив этот метод, мы можем вызвать его в контексте следующим образом:
modelBuilder.Types()
.Configure(c => c.ToTable(GetTableName(c.ClrType)));
Это соглашение настраивает каждый тип в нашей модели для сопоставления с именем таблицы, возвращаемой из метода GetTableName. Это соглашение эквивалентно вызову метода ToTable для каждой сущности в модели с помощью API Fluent.
Обратите внимание на то, что при вызове ToTable EF будет принимать предоставленную вами строку как точное имя таблицы, без какой-либо плюрализации, которую он обычно выполняет при определении имен таблиц. Именно поэтому в нашем соглашении по именам таблиц используется имя product_category вместо product_categories. Мы можем решить это в соглашении, обратившись к службе плюрализации.
В следующем коде мы будем использовать механизм разрешения зависимостей, добавленный в EF6, чтобы получить службу плюрализации, которая использовалась бы EF для приведения имени таблицы к форме множественного числа.
private string GetTableName(Type type)
{
var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();
var result = pluralizationService.Pluralize(type.Name);
result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
Замечание
Обобщённая версия GetService — это метод расширения в пространстве имен System.Data.Entity.Infrastructure.DependencyResolution. Вам необходимо добавить директиву using в ваш код, чтобы её использовать.
ToTable и наследование
Еще одним важным аспектом ToTable является то, что при явном сопоставлении типа с заданной таблицей можно изменить стратегию сопоставления, которую будет использовать EF. Если вы вызываете ToTable для каждого типа в иерархии наследования, передавая имя типа в качестве имени таблицы, как показано выше, вы измените стратегию сопоставления по умолчанию Per-Hierarchy (TPH) на Table-Per-Type (TPT). Лучший способ описать это — конкретный пример:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Manager : Employee
{
public string SectionManaged { get; set; }
}
По умолчанию сотрудники и менеджер сопоставляются с одной таблицей (Employees) в базе данных. Таблица будет содержать как сотрудников, так и руководителей со столбцом-дискриминатором, который указывает, какой тип экземпляра хранится в каждой строке. Это сопоставление TPH, так как существует одна таблица для иерархии. Однако при вызове ToTable в обоих классах каждый тип вместо этого будет сопоставлен с собственной таблицей, также известной как TPT, так как каждый тип имеет собственную таблицу.
modelBuilder.Types()
.Configure(c=>c.ToTable(c.ClrType.Name));
Приведенный выше код сопоставляется со структурой таблицы, которая выглядит следующим образом:
Вы можете избежать этого или поддерживать сопоставление TPH по умолчанию несколькими способами:
- Выполните вызов ToTable с тем же именем таблицы для каждого типа в иерархии.
- Вызывайте ToTable только в базовом классе иерархии; в нашем примере это будет класс Employee.
Порядок выполнения
Соглашения работают по принципу "последний выигрыш", так же, как и Fluent API. Это означает, что если вы напишете два соглашения, которые настраивают одну и ту же опцию одного и того же свойства, то победит то, которое выполняется последним. Например, в коде ниже максимальная длина всех строк установлена на 500, но затем мы настраиваем все свойства модели с именем "Name", чтобы их максимальная длина составляла 250.
modelBuilder.Properties<string>()
.Configure(c => c.HasMaxLength(500));
modelBuilder.Properties<string>()
.Where(x => x.Name == "Name")
.Configure(c => c.HasMaxLength(250));
Так как соглашение о задании максимальной длины до 250 применяется после того, как все строки установлены длиной 500, все свойства в нашей модели, называемые Name, будут иметь MaxLength 250, тогда как остальные строки, такие как текст описания, будут иметь длину 500. Использование соглашений таким образом означает, что вы можете предоставить общее соглашение для типов или свойств в модели, а затем переопределите их для подмножеств, которые отличаются.
API Fluent и заметки к данным также можно использовать для переопределения соглашения в определенных случаях. В нашем примере выше, если бы мы использовали Fluent API для задания максимальной длины свойства, то могли бы поместить его до или после конвенции, так как более специфичный Fluent API будет иметь приоритет над более общей конвенцией конфигурации.
Встроенные соглашения
Так как пользовательские соглашения могут влиять на соглашения Code First по умолчанию, это может быть полезно для добавления соглашений для запуска до или после другого соглашения. Для этого можно использовать методы AddBefore и AddAfter коллекции Conventions в производном DbContext. Следующий код добавит класс соглашения, созданный ранее, чтобы он выполнялся перед встроенным соглашением об обнаружении ключей.
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
Такой подход будет наиболее полезен при добавлении соглашений, которые должны выполняться до или после встроенных соглашений. Список встроенных соглашений можно найти здесь: System.Data.Entity.ModelConfiguration.Conventions Namespace.
Вы также можете удалить соглашения, которые не нужно применять к модели. Чтобы удалить соглашение, используйте метод Remove. Ниже приведен пример удаления PluralizingTableNameConvention.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}