Entity Framework. Часть 5 – Fluent API

Продолжим изучение возможностей Entity Framework Code First для указания соотношений классов Модели и создаваемых таблиц. На очереди второй способ – использование Fluent API.

Потоковый интерфейс (Fluent API)

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

  • Методы возвращают объекты, что позволяет вызывать их цепочкой в рамках одного оператора.
  • Имена методов подбираются так, чтобы обеспечить максимально понятный код.

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

modelBuilder.Entity<Gamer>()
    .HasRequired(e => e.ReserveTeam)
    .WithMany(t => t.Reserve)
    .HasForeignKey(e => e.ReserveId)
    .WillCascadeOnDelete(false);

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

в сущности, описанной классом Gamer 
.есть обязательное навигационное поле ReserveTeam
.связанное с коллекцией Reserve в другой сущности (таблице)
.при помощи внешнего ключа ReserveId
.без каскадного удаления записей

Кроме того, такой подход очень удобен при использовании совместно с контекстными подсказками, такими как IntelliSense в Visual Syudio. В этом случае они наглядно указывают возможные дальнейшие действия в зависимости от текущего контекста.

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

В этой серии статей под Fluent API будем понимать, как правило, реализацию интерфейса Entity Framework Code First для настройки параметров генерации таблиц.

Использование Entity Framework Fluent API

Как уже было отмечено, при использовании Entity Framework Fluent API нет необходимости модифицировать Модель. Параметры создания таблиц определяются программно. Необходимый для этого код размещается внутри метода OnModelCreating(DbModelBuilder modelBuilder), переопределенного в соответствующем классе контекста базы данных.

Построение выражений для каждого нового правила начинается с экземпляра DbModelBuilder, полученного в качестве параметра. Его метод Entity<T>() возвращает объект типа EntityTypeConfiguration. Он, в свою очередь, используется для настройки параметров генерации таблицы для сущности указанного типа T. Вот пример кода на основе класса контекста CatalogContext из создаваемого веб-приложения:

namespace BookCatalog.Models.DbContext
{
    using System.Data.Entity;
    using BookCatalog.Models;

    public class CatalogContext : DbContext
    {
        // You can add custom code to this file. Changes will not be overwritten.
        // 
        // If you want Entity Framework to drop and regenerate your database
        // automatically whenever you change your model schema, add the following
        // code to the Application_Start method in your Global.asax file.
        // Note: this will destroy and re-create your database with every model change.
        // 
        // System.Data.Entity.Database.SetInitializer(new System.Data.Entity.DropCreateDatabaseIfModelChanges<BookCatalog.Models.CatalogContext>());

        public DbSet<Tag> Tags { get; set; }

        public DbSet<Publisher> Publishers { get; set; }

        public DbSet<Language> Languages { get; set; }

        public DbSet<BookDetails> Books { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // место для вызовов Entity Framework Fluent API
        }
    }
}

В приведенном примере отмечено место добавления вызовов Fluent API. Их, в зависимости от цели, можно разделить на три группы:

  • Настройка свойств;
  • Настройка типов;
  • Настройка взаимосвязей между сущностями.

Давайте рассмотрим каждую группу в отдельности подробнее и с небольшими примерами.

Настройка свойств с использованием Fluent API

Для настройки параметров соотношения отдельных свойств и колонок таблицы в базе данных класс EntityTypeConfiguration предоставляет метод Property(). Он возвращает объект, являющийся одним из наследников класса PrimitivePropertyConfiguration, которые и содержит необходимые методы. Посмотрим на некоторые из них на примерах.

Указание первичного ключа

И начнём сразу с исключения. Дело в том, что первичный ключ задается вызовом HasKey() класса EntityTypeConfiguration. Для примера, укажем в этой роли свойство Id сущности BookDetails:

modelBuilder.Entity<BookDetails>().HasKey(b => b.Id);

В качестве параметра передается лямбда-выражение, которое возвращает значение свойства, выбранного для первичного ключа. На основе полученного параметра, ядро Entity Framework определит необходимое свойство и создаст первичный ключ на его основе. Стоит отметить, что именно так во многих методах Fluent API указываются свойства для настройки.

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

modelBuilder.Entity<Publisher>().HasKey(p => new { p.Title, p.Homepage } );

Атрибут-аналог: [Key].

Ограничение длины строки

Ограничим длину свойства Title класса BookDetails в 128 символов:

modelBuilder.Entity<BookDetails>().Property(p => p.Title)
    .HasMaxLength(128);

Атрибут-аналог: [MaxLenght].

Обязательное к заполнению свойство

Также для указанного в прошлом примере свойства установим необходимость всегда содержать значение

modelBuilder.Entity<BookDetails>().Property(p => p.Title)
    .IsRequired();

Атрибут-аналог: [Require].

Установка способа генерации значения

Предположим, что для свойства Id класса BookDetails значения будет указывать бизнес-логика самостоятельно. Тогда необходимо отключить их генерирование на стороне базы данных:

modelBuilder.Entity<BookDetails>().Property(p => p.Id)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

Атрибут-аналог: [DatabaseGenerated].

Исключение свойства при создании таблицы

Предположим, что в классе BookDetails есть свойство TotalTagsLenght. Его не требуется включать в базу данных, так как это значение вычисляется бизнес-логикой веб-приложения. Указать это можно следующим способом:

modelBuilder.Entity<BookDetails>().Ignore(t => t.TotalTagsLenght);

Атрибут-аналог: [NotMapped].

Указание имени для колонки в базе данных

Для свойства PublishedAt класса BookDetails установим текст "PublishDate" в качестве имени колонки:

modelBuilder.Entity<BookDetails>().Property(t => t.PublishedAt)
    .HasColumnName("PublishDate");

Атрибут-аналог: [Column].

Указание использовать или не использовать уникод для текстового поля

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

modelBuilder.Entity<BookDetails>().Property(t => t.Author)
    .IsUnicode(false);

Атрибут-аналог: [Column] c явным указанием типа колонки.

Указание порядка колонки в таблице

Указать порядковый номер колонки в таблице можно используя метод HasColumnOrder():

modelBuilder.Entity<BookDetails>().Property(t => t.Author)
    .HasColumnOrder(2);

Атрибут-аналог: [Column] c явным указанием порядкового номера.

Указание использовать определенный тип для колонки в базе данных

Fluent API позволяет указать тип, который должна содержать связанная со свойством колонка:

modelBuilder.Entity<BookDetails>().Property(t => t.Author)
    .HasColumnType("varchar");

Атрибут-аналог: [Column] c явным указанием типа колонки.

Конфигурация вложенных типов

Рассмотрим ситуацию, когда класс Customer включает в себя экземпляр класса Address. В последнем есть свойство Street, которому необходимо установить максимальную длину строки в 128 символов. Такая задача легко решается с помощью Fluent API:

modelBuilder.Entity<Customer>().Property(t => t.Address.Street)
    .HasMaxLength(128);

Стоит отметить, что основное преимущество Fluent API проявляется при необходимости указать несколько параметров соотношения для одного свойства. И несмотря на то, что в примерах выше настраивается по одному свойству, следующая запись корректна:

modelBuilder.Entity<BookDetails>().Property(t => t.Title)
    .IsRequired()
    .HasMaxLength(128)
    .HasColumnName("BookTitle")
    .HasColumnOrder(2);

Оставим небольшое число методов для самостоятельного изучения и перейдем к настройке параметров отображения для типов.

Настройка типов с использованием Fluent API

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

Указание имени таблицы

Допустим необходимо присвоить имя "Catalog" для таблицы, в которой будут располагаться данные из экземпляров класса BookDetails. Код этой настройки будет следующий:

modelBuilder.Entity<BookDetails>().ToTable("Catalog");

Атрибут-аналог: [Table].

Исключение типа при создании таблицы

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

modelBuilder.Ignore<Tag>();

В данном случае экземпляры класса Tag не будут сохраняться в базе данных. Равно как и не будет создана соответствующая таблица.

Указание сложных типов

Entity Framework Code First также автоматически распознает сложные типы. Однако в некоторых случаях могут быть ошибки. Например, если в таком классе есть поле Id для каких-либо целей. Тогда потребуется указать его с помощью FluentAPI:

modelBuilder.ComplexType<SomeComplexType>(); 

Настройка отображения "Таблица для иерархии (Table per hierarchy)"

При использовании данного типа отображения наследования, для всех классов, унаследованных от общего базового, создается единая таблица. В Code First это вариант по умолчанию. При этом в таблицу добавляется колонка под названием Discriminator, в которой указывается имя конкретного типа для каждой строки. Переопределить такое поведение можно следующим образом:

modelBuilder.Entity<BaseType>() 
    .Map<BaseType>(m => m.Requires("Type").HasValue("BaseType")) 
    .Map<ChildType>(m => m.Requires("Type").HasValue("ChildType"));

Настройка отображения "Таблица для каждого конкретного типа (Table per concrete type)"

Тип отображения наследования "таблица для каждого конкретного типа" подразумевает, что создаются индивидуальные таблицы для каждого конкретного типа. Определить такое поведение при помощи Fluent API можно следующим образом:

modelBuilder.Entity<BaseType>()
    .Property(p => p.Id)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);

modelBuilder.Entity<ChildType>().Map(m =>
{
    m.MapInheritedProperties();
    m.ToTable("ChildType");
});

Настройка отображения "Таблица для каждого типа (Table per type)"

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

modelBuilder.Entity<BaseType>().ToTable("BaseType"); 
modelBuilder.Entity<ChildType>().ToTable("ChildType");

Разделение одного типа по нескольким таблицам

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

Предположим, что свойства Id, Title, Author и Url класса BookDetails запрашиваются наиболее часто. Поэтому расположим их в отдельной таблице:

modelBuilder.Entity<BookDetails>()
    .Map(m => {
        m.Properties(b => new { b.Id, b.Title, b.Author, b.Url });
        m.ToTable("Catalog");
    })
    .Map(m => {
        m.Properties(b => new { 
            b.Description,
            b.IsFree,
            b.IsVisible,
            b.LanguageId,
            b.PublishedAt,
            b.PublisherId,
            b.Rating });
        m.ToTable("BookDetails");
    });

Объединение нескольких классов в одну таблицу

Данный сценарий может пригодиться для оптимизации в случае связи "одна строка к одной". Например, пусть каждый судья (класс Arbitter) может быть назначен только на одну игру (класс Game). Тогда код Fluent API будет выглядеть следующим образом:

modelBuilder.Entity<Game>()
    .HasKey(g => g.ArbitterID);
            
modelBuilder.Entity<Arbitter>()
    .HasRequired(a => a.Game)
    .WithRequiredPrincipal(t => t.Arbitter);

modelBuilder.Entity<Arbitter>().ToTable("Games");
modelBuilder.Entity<Game>().ToTable("Games");

В следующей части тема настройки Модели для Code First будет продолжена. В ней рассмотрим возможности Fluent API для указания связей между таблицами.

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

Павел 30.06.2011 13:41:12

Андрей, а Вы книгу, часом, выпускать не собираетесь?=)

@ Павел: Нет. Хотя иногда мысль такая появляется.

@ Andrey:
Мысль очень хорошая. У вас хорошо получается выбирать из материала самое важное и потом понятно/наглядно это доносить до читателей.
И если время на большую книгу может не хватить, то вот лично я, от книги на руках в формате краткого справочника/руководства/faq не отказался.)) Ваш блог уже выполняет для меня роль такой книги. Осталось немного!!!

Артем 15.08.2011 19:43:00

Присоединяюсь, в Вас определенно скрыт не дюжий литературный талант, пишите побольше нам на радость!=)

Ринат 21.08.2011 1:39:33

Андрей, огромное спасибо вам за этот блог, присоединяюсь ко всему что сказано выше
PS: от хотя бы pdf-версии не отказался бы)

Спасибо за теплые слова.  Про "литературный талант" это конечно перебор (особенно про "литературный"). А вот про PDF версию ... очень навряд ли. Я сейчас практически не могу найти время на продолжение серии, т.к. вплотную занят текущими проектами. Хоть ищи помощника для работы над задачами.

Ринат Муллаянов 21.08.2011 21:34:56

Небольшая опечатка: "В качестве пермеаметра передается лямбда-выражение"...

@ Ринат Муллаянов: Спасибо. Поправил.

Анатолий 15.09.2011 23:00:26

Андрей, добрый день Smile
Спасибо за статью. Очень интересно.

У меня есть вопрос касательно создания таблиц с использованием EF Code First.
Предположим есть следующий код:

    public abstract class Document
    {
       public int DocId {get; set;}
       public DocumentType DocType {get; set;}
       public DateTime DateCreated {get; set;}
       public DateTime DateUpdated {get; set;}

    }

    public class DocumentType
    {
      public int TypeId {get; set;}
      public int TypeName {get; set;}
    }


    public class News: Document
    {
       public string Title {get; set;}
       public string Body {get; set;}
    }

Я хочу создать БД, в которой было бы несколько базовых таблиц типа Document, DocumentType и ещё несколько. News наследуется от Document и как бы является  документом у которого DocumentType есть "News".
Я на данный момент не знаю, какие ещё таблицы у меня будут в БД. Но хотелось бы иметь возможность при добавлении новой таблицы, которая "наследуется" от Document, чтоб автоматически сохранялась запись о новом типе документа в таблицу DocumentType. Например, если мне понадобилось добавить таблицу Comment (которая наследуется от Document), чтоб автоматом добавлялась запись в новом типе в DocumentType.
Можно ли такое осуществить в EF Code First ?

Спасибо. Буду очень рад, если вы поможете Smile

P.S Отличный у Вас блог.

Если я вас правильно понял, то DocumentType будет запоняться по мере появления записей с новым TypeId. Если надо сделать таблицу вообще всех возможных типов - то заполните её вручную.

Кроме того, в зависимости от настройки для классов-наследников могут создаваться отдельные таблицы, а могут и нет. Это можно определи��ь через FluentAPI.

Извините, может я ошибаюсь. Но не перепутан ли код?

"Table per concrete type" -подразумевает, что создаются индивидуальные таблицы для каждого конкретного типа. Определить такое поведение при помощи Fluent API можно следующим образом:

  modelBuilder.Entity<BaseType>().ToTable("BaseType");
  modelBuilder.Entity<ChildType>().ToTable("ChildType");

- Данный код у меня создал 2 таблицы.  В первой только свойства BaseType.  Во второй  только ChildType.


"Таблица для каждого типа (Table per type)"

Этот вариант отображения наследования предусматривает, что создается одна таблица на основе базового типа для общих свойств. Все остальные размещаются в вспомогательных таблицах. Задается такая схема следующим образом:
  modelBuilder.Entity<BaseType>()
      .Property(p => p.Id)
      .HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
  
  modelBuilder.Entity<ChildType>().Map(m =>
  {
      m.MapInheritedProperties();
      m.ToTable("ChildType");
  });


Этот код создал кокраз создал 2 таблицы в первой только св-ва Base.  Во второй Base+Child. т.е. индивидуальные таблицы для каждого типа.

@ Юрий: Спасибо за найденную опечатку.

Это вам огромное спасибо за материал!
Если вам не трудно, помогите разобрать – баг это в EF или нет?  Не буду описывать всё понятно из кода. Модель:
public class Category
    {
        public int CategoryId { get; set; }
        public string Name { get; set; }
        public virtual ICollection<Product> Products { get; set; }
    }
    public class Product
    {
        public int ProductId { get; set; }
        public string NamePr { get; set; }
        public int CategoryId { get; set; }
        public virtual Category Category { get; set; }
    }
    public class ProductPlus : Product
    {
        public string NamePr2 { set; get; }
    }  
    public class ProductContext : DbContext
    {
        public ProductContext()
            : base("MyBd") { }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Category>().ToTable("Category");
            modelBuilder.Entity<Product>().ToTable("Product");
            modelBuilder.Entity<ProductPlus>().ToTable("ProductPlus");//наследник        
        }
        public DbSet<Category> Categorys { get; set; }        
    }
Теперь выполняю код создания :Main>>
Product p = new Product();
p.NamePr = "Продукт1";
ProductPlus p2 = new ProductPlus();
p2.NamePr = "Продукт2"; p2.NamePr2 = "Продукт2_";
Category c = new Category();
c.Name = "Категория";
c.Products = new System.Collections.Generic.List<Product>();
c.Products.Add(p);
c.Products.Add(p2);
bd.Categorys.Add(c);
bd.SaveChanges();
bd.Dispose();

Всё отлично, база создалась, записи добавлены и “расфасованы”

Далее следующий код, пытаюсь удалить категорию, при выполнение кода не “заглядывать” в экземпляры!! Иначе не получится:

ProductContext bd = new ProductContext();            
Category c2 = bd.Categorys.First(); //1
bd.Categorys.Remove(c2); //3
bd.SaveChanges(); //4

На четвёртой строчке происходит ошибка : “При обновлении записей произошла ошибка. Подробные сведения см. во внутреннем исключении.”
Далее после первой строчки вставляем:

int t = c2.Products.Count;  //2  (конечно оптимизацию отключить)

Т.е. просто считаю свойство, но можно и просто при отладке “заглянуть” в объект
….
И …  Всё прокатывает – всё удаляется и сохраняется…

EF 4.2  (на 4.1 update 1 аналогично).    
Очевидно, что только после обращения к экземпляру EF что то доделывает после чего удаление происходит правильно.
Что вы по этому поводу думаете? Спасибо.  

@ Юрий: В первую очередь вопрос - а что указано во внутреннем исключении? Обычно проходя их цепочку до конца можно понять суть ошибки.

innerException ="Конфликт инструкции DELETE с ограничением REFERENCE \"ProductPlus_TypeConstraint_From_Product_To_ProductPlus\". Конфликт произошел в базе данных \"MyBd\", таблица \"dbo.ProductPlus\", column 'ProductId'.\r\nВыполнение данной инструкции было прервано."}

Но я об том, почему если перед удалением считать что нибудь из экземпляра, то ошибки нет

@ Юрий: Из этого сообщения суть не ясна? Просто на развернутый ответ надо время. Если не поняли - вечером постараюсь ответить подробно почему так происходит.

Ну если не очень трудно, то как сделать правильно при таком наследовании (без наследования ошибки не возникает) ?

@ Юрий: Опубликовал описание ситуации в статье
andrey.moveax.ru/.../...ect-hierarchy-mapping.aspx

Константин 11.04.2012 21:10:20

Андрей, подскажите, как можно сделать отображение одной модели на несколько БД с помощью EF, если это возможно? Спасибо!

@ Константин: Не пробовал такое, но первая мысль - а что мешает создать несколько контекстов? Если же хочется "один раз сохранил и сразу везде", то я бы смотерел на решения на стороне БД.

Алексей 28.12.2012 16:56:49

Спасибо за серию статей, хорошо расписано. Хотелось бы ещё статей по организации миграции, при изменении модели в подходе Code First. Так же список рекомендуемых  книг по EF.

Алексей 28.12.2012 17:28:55

Упс, по поводу миграции уже статья есть Smile

Роман 08.05.2013 18:29:01

Андрей, доброго дня.
Скажите пожалуйста во Fluent API и в CodeFirst есть свойство уникальности для полей? Если нет, то как его можно сделать?
не нашел ни IsUnique() ни [Unique]
Гугл говорит записью от 2011 года, что обещали добавить в следующей версии, я так понимаю не добавили?

Атрибута нет. Установить Unique для поля можно в методе Seed инициализатора БД. Что-то вроде

public class MyInitializer : CreateDatabaseIfNotExists<MyContext>
{
   protected override void Seed(MyContext context)
   {
      context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX IX_Title ON Title");
   }
}

Александр 04.09.2013 19:06:36

Добрый день, Андрей!
Вопрос по разделению одного типа по нескольким таблицам. Как в этом случае организовать доступ к данным так, чтобы обращение шло только к одной таблице? Например, в BookDetails нас интересуют только поля Id, Title, Author, Url , т.е. столбцы таблицы Catalog.

@ Александр: С помощью перечисления нужных свойств в Select().

Александр 05.09.2013 16:10:47

@ Andrey:
Не могли бы вы привести пример.
В классе BookDetailsRepository у вас есть метод:

public BookDetails Find(int id)

{
  return this._context.Books.Find(id);
}

Преобразовал его следующим образом:
public BookDetails Find(int id)

{
  return this._context.Books.Select(b =>
    new BookDetails{Id = b.Id,Title = b.Title,Author = b.Author,Url=b.Url}).First(b => b.Id == id);
}
Но при вызове метода получаю сообщение об ошибке:
NotSupportdeException. В запросе LINQ to Entities нельзя конструировать тип сущности или сложный тип "CodeFirst.BookDetails"

@ Александр: В начале используйте анонимный класс, а уже потом (результат) перенесите в BookDetails.

Александр 05.09.2013 17:31:43

@ Andrey:
переделал метод следующим образом:

public BookDetails Find(int id)

{
  var book = this._context.Books.AsQueryable()
                .Select(b => new {b.Id, b.Title, b.Author, b.Url}).FirstOrDefault(b => b.Id == id);
  return (book == null) ? null : new BookDetails(book.Id, book.Title, book.Author, book.Url);
}

Но через профайлер вижу следующий запрос:
exec sp_executesql N'SELECT TOP (1) 

[Extent1].[Id] AS [Id],
[Extent1].[Title] AS [Title],
[Extent1].[Author] AS [Author],
[Extent1].[Url] AS [Url]
FROM  [dbo].[Catalog] AS [Extent1]
INNER JOIN [dbo].[BookDetails] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
WHERE [Extent1].[Id] = @p__linq__0',N'@p__linq__0 int',@p__linq__0=22

Из-за использования Inner Join происходит поиск по кластеризованному индексу таблицы BookDetails - а это как раз то, чего мне хотелось избежать.

@ Александр: Перепроверил на простом примере - вы правы (вижу схожий запрос). Если select в обычной ситуации работает как надо, то тут EF все равно добавляет ненужный INNER JOIN. Больше смахивает на ошибку в библиотеке. Добавил им в issues. https://entityframework.codeplex.com/workitem/1630

Александр 10.09.2013 0:06:19

@ Andrey:Спасибо!
Можно ли в рассматриваемом примере получить список книг по списку переданных пар, например, (Author, Url), то есть что-то вроде

    private List<BookDetails> GetByFilter<TProperty>(List<BookFilter> bookFilters)

    {
        var query = _context.Set<Book>() as IQueryable<BookDetail>;

        return query.Where(b => bookFilters.Contains(new BookFilter{Author = b.Author, Url= b.Url})).ToList();
    }

Мне попалась ссылка на построитель предикатов, где в цикле строится длинное условие поиска:
www.albahari.com/nutshell/predicatebuilder.aspx
Есть способ лучше?
В идеале хотелось бы передать в качестве параметра таблицу, соединение с которой по полям Author и Url возвращало бы запрашиваемый результат. Смотреть в сторону использования хранимой процедуры?

@ Александр: Я бы смотрел или на предикаты (шаблон Specifications) или на QueryObject. Первые хорошо подходят когда сам запрос заранее не известен. Второй шаблон - это создание отдельных QueryObject для запросов данных из хранилища.

Хранимая процедура хороша только в том случае, если она дает заметный прирост скорости. При этом IMHO не стоит обращаться к ней напрямую, а использовать например тот же QueryObject. Т.е. BL не должен знать подробности организации хранения данных.

Сергей 08.10.2013 2:20:40

Андрей, доброго времени суток.
Огромное вам спасибо за ваши прекрасные статьи!

В процессе работы возникла проблема, сейчас кратко попробую описать ситуацию
Есть уже существующая база, структуру которой менять нельзя, в ней две таблицы, у обеих таблиц есть РК типа uniqueidentifier, например таблица Products (Id<uniqueidentifier>, CurrencyId<nvarchar(40)>) и таблица Currency (Id<uniqueidentifier>). Как вы можете увидеть таблица Products содержит текстовое поле с ссылкой на таблицу Currency. В самой базе данных никакой связи нет. Непосредственно вопрос - можно ли, используя Fluent API, настроить связь в модели EF, чтобы якобы между таблицами существует связь один ко многим, хотя бы для выборки данных, сохранение не обязательно.

Заранее спасибо.

@ Сергей: Пожалуйста.

И ответ на ваш вопрос: насколько я понимаю - нельзя, т.к. указание такой связи в Fluent приведет к необходимости создания полей соответствующих типов и внешних ключей. Может быть как-то и можно обмануть EF, но я бы лучше применил Join в запросах.

Сергей 08.10.2013 12:58:11

@ Andrey:
Спасибо, за быстрый ответ.
Буду тогда join'ить Smile

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

@ Леша Пожалуйста.

Подскажите, пожалуйста, не понял по Настройка отображения "Таблица для иерархии (Table per hierarchy)"

Например, у нас есть базовый класс
Person
{
         public Guid ID {}
         public string name {get; set; }
}

есть от него наследники

Worker : Person
{
         public int Number { get; set; }
}


и

Student :  Person
{
      public string  NikName {get; set;}
}


В БД есть таблица по имени People, которая хранит информацию о данных сущностях.

Как собственно отразить данные из таблицы People на сущности Student  и Worker?

Андрей, подскажите, пожалуйста,  как отобразить данные в следующей ситуации. Например, у нас есть базовый класс А, от которого наследуются классы В и С.

class A
{
      public string A {get; set; }
}

class B : A
{
      public string B {get; set; }
}

class C : A
{
      public string C {get; set; }
}

В БД, указанная информация хранится следующим образом. Есть базовая таблица А, которая связана связью "Один к Одному" с таблицами В и С.

Для задания такого рода привязки, я делал следующее.

1 Создал класс конфигурации для требуемой дочерней сущности, например для A.
class A:
        EntityTypeConfiguration<A>
    {
        public LibraryEntityConfig()
        {
            this.Map(e => {
                e.ToTable("A");
            });
        }
    }

2 Создал класс конфигурации для требуемой дочерней сущности, например для B.

class B:
        EntityTypeConfiguration<B>
    {
        public LibraryEntityConfig()
        {
            this.Map(e => {
                e.ToTable("B");
            });
        }
    }

Подскажите, так правильно привязывать подобного ��ода сущности. Может что-то некорректно настроил? Все вроде работает, данные сущности "B" сохраняются и извлекаются. Но смущает несколько фактов:

1 ) В таблице "А" в поле Discriminator при сохранении сущности "B" записывается значение "undefined". Хотя я ожидал, что там будет указано имя типа, т.е. "В".

2) В таблице "B"  пришлось отключить автоинкремент у первичного ключа, который связан с таблицей "A"  связью "Один к Одному". Хотя я ожидал, раз сущность "B" наследуется от "A", то  при сохранении ее в БД, EF не будет пытаться задать в таблице "B" значение первичного ключа самостоятельно, а предоставит это SQL Server.

@ Ps Если я правильно ваш первый вопрос - то так как TPH это вариант по умолчанию, то в работе программы нужно просто загрузить сущности Worker и Student.

Что касается второго вопроса - у вас в одной таблице хранятся А и С, вот их и разделяет Discriminator. Тип B лежит в другой таблице и не требует особых отметок (и так ясно что это если обращаетесь к табл. B).

Ну и EF не создает ключ сам, это как раз делает БД. Опция как раз передает в БД запрет на это действие. Так что да - предоставлено SQL и требует отключения.

Александр 21.02.2015 5:09:15

У меня проблема: не могу объединить две сущности. Есть модели двух таблиц Картинки и КартинкиВАльбоме:
public partial class ImagesInAlbum
    {
        public int Id { get; set; }
        public int IdAlbum { get; set; }
        public int IdImage { get; set; }
    
        public virtual Album Album { get; set; }
        public virtual Image Image { get; set; }
    }
и класс картинок
public partial class Image
    {
        public Image()
        {
            this.ImagesInAlbums = new HashSet<ImagesInAlbum>();
            this.Albums = new HashSet<Album>();
        }
    
        public int Id { get; set; }
        public string ImageName { get; set; }
        public string TypeImage { get; set; }
        public System.DateTime DateImage { get; set; }
        public string SmallImage { get; set; }
        public Nullable<int> UserId { get; set; }
    
        public virtual ICollection<ImagesInAlbum> ImagesInAlbums { get; set; }
        public virtual ICollection<Album> Albums { get; set; }
    }
Мне надо выбрать картинки из определенного альбома. Как это сделать? Класс Image уже имеет (или должен иметь) данные из двух других классов:
        this.ImagesInAlbums = new HashSet<ImagesInAlbum>();
        this.Albums = new HashSet<Album>();
Если просто вывести данные класса Image, то естественно, будут взяты данные из этой таблицы. А как заполнить еще данными из ImagesInAlbums, чтоб потом можно было отсортировать конкретный альбом?

@Александр: Я так понимаю вам нужен Include при запросе? Т.е. запрос будет вида


context.ImagesInAlbum
    .Include(i => i.Image)
    .Where(i => i.IdAlbum == someId)
    .ToArray()

Konstantin 03.07.2015 16:54:47

Добрый день! Присоединяюсь к комментариям указанным выше)) действительно очень понятно описали эту тему. Вопрос такой можно ли как-то с помощью fluent-api либо атрибутов либо еще как-то указать дефолтовое значение в колонке? Уже весь инет перекопал, но ничего не нашел, будто этим никто и не пользуется. Есть только параметр в функции AlterColumn, но это только при миграции, а нужно чтобы именно при создании таблицы в code first. Подскажите пожалуйста))

@Konstantin К сожалению порадовать вас нечем. Насколько мне известно, то это возможно сделать только в миграции.

Елена 27.10.2015 20:47:00

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

            modelBuilder.Entity<ApplicationUser>()
                 .ToTable("User")
                .Map(e => e.Requires("IsDeleted").HasValue(false))
                .Ignore(e => e.IsDeleted);

в момент выполнения следующая ошибка:
"Map was called more than once for type 'ApplicationUser' and at least one of the calls didn't specify the target table name."
не подскажете, что я делаю не так? ApplicationUser унаследован от класса IdentityUser из aspnet Identity.

так же имеются еще такие настройки:
modelBuilder.Entity<ApplicationUser>()
                    .HasMany(e => e.UserSwimmers)
                    .WithRequired(e => e.User)
                    .HasForeignKey(e => e.UserId);

            modelBuilder.Entity<ApplicationUser>()
                .HasMany(e => e.UserProfile)
                .WithRequired(e => e.User)
                .HasForeignKey(e => e.UserId)
                .WillCascadeOnDelete(false);

Как я вижу вы хотите сделать фильтр, чтобы не писать всех запросах проверку на IsDeleted. Не пробовал подобное, но что-то мне подсказывает что надо внутри Map тогда тоже указать имя таблицы. Не пробовали?
Навскидку, но не уверен что сработает: Map(e => e.ToTable("Users").Requires("IsDeleted").HasValue(false))

Елена 30.10.2015 14:19:29

Пробовала по-разному, и предложенный вариант тоже. Так и не удалось для пользователей  реализовать так называемое "мягкое удаление" (((

Андрей 03.03.2016 12:05:32

использую привязку к хранимым процедурам в Code First  MapToStoredProcedures. Содержимое хранимок генерируется атоматически - не подскажет ли кто как изменить sql-код, который генерируется для создания этих хранимок? - умаяло после пересоздания БД запускать скрипт пересоздания хранимок.

Андрей А если создать свой класс стратегии инициализации БД? (реализация IDatabaseInitializer<TContext>)

Добрый день. Хочу обратиться к Вам за советом.
Есть несколько микросервисов которые шарят между собой одну базу данных. У каждого сервиса своя модель! Т.е. например:
- service#1 содержит User с полями Id, Username и Password
- service#2 содержит User с полями Id, Firstname и Lastname
- service#3 тоже содержит User с полями Id, Firstname, Lastname, Birthdate и еще какие-то...

Ни один сервис не должен знать о существовании другого и должен работать только со своей моделью. Но данные должны храниться в одной таблице Users. Каждый сервис содержит свой Data Access Layer, работающий со своими моделями, но подключающийся к одной и той же базе. Поля сущности User во всех сервисах или дополняют общий набор полей или определяются по одним и тем же правилам. Т.е. нет такого, что в одном сервисе написано Property(x => x.FirstName).HasMaxLength(100), а в другом Property(x => x.FirstName).HasMaxLength(90)

Каким образом можно организовать конфигурацию этих сущностей? Т.е. можно ли обеденить несколько классов EntityTypeConfiguration<User> из разных проектов с одним названием сущности и названием таблици, например в один проект и от туда запускать миграцию? Или каким-то другим способом решить такую задачу?

Я не нашел другого способа, как писать длинный и страшный SQL-DDL с кучей проверок.

Спасибо!

Roman Мне кажется объединение C# кода в отдельном проекте в данном случае создаст больше проблем, чем использование подхода DatabaseFirst. Причем не обязательно создавать сущности по БД как в оригинальном и уже устаревшем варианте DatabaseFirst через Wizard.

Я бы просто создавал БД в отдельном SQL проекте. Там же бы его поддерживал и вел миграции. А из кода просто подключался бы к БД на манер CodeFirst только с отключенными CodeFirst миграциями и генерацией БД.

Честного говоря такой подход даже в общем случае мне кажется более удачным, т.к. позволяет использовать и прозрачность CodeFirst и фишки БД, которые через CodeFirst не доступны и скорее всего никогда там не будут (ибо это специфика конкретной БД).

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