Entity framework объединение таблиц

Обновлено: 04.07.2024

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

Подход с использованием нескольких классов модели для одной таблицы скорее всего понадобится, когда вы работаете с уже существующей базой данных (т.е. используете Code-Second). Хотя никто вас не ограничивает использовать множественные сущности при работе с Code-First, когда на их основе будет создаваться одна таблица в базе данных.

Допустим у нас имеется следующая модель:

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

Между сущностями должно быть реализовано отношение один-к-одному.

Они должны использовать общий ключ.

Наши классы Customer и Photo реализуют эти правила, поэтому мы можем настроить их для использования в одной таблице базы данных. Для этого можно использовать атрибут Table в модели для обоих классов, передав ему имя одной таблицы:

Обратите внимание, что в классе Customer мы указываем атрибут Required для навигационного свойства, ссылающегося на таблицу Photo. Благодаря этому мы указываем Entity Framework на создание связи один-к-одному, иначе, была бы создана связь ноль-или-один-к-одному, что не соответствовало бы первому условию. Если вы теперь запустите приложение, то Entity Framework воссоздаст базу данных, т.к. модель данных изменилась, и использует одну таблицу Customer для двух сущностных типов:

Создание одной таблицы для двух сущностей

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

Если вы используете запрос наподобие следующего:

то Entity Framework выберет данные покупателя без данных о фотографии, несмотря на то, что эти данные хранятся в одной таблице базы данных. Это говорит о том, что в логически в коде Entity Framework фактически работает с двумя таблицами. Чтобы извлечь также данные о фотографии нужно использовать метод Include():

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

Альтернативным способом настройки множественных сущностей является использование Fluent API. Как описывалось в предыдущей статье, для явного указания имени таблицы в Fluent API используется метод ToTable():

Несколько таблиц для одной сущности

Теперь давайте рассмотрим обратный сценарий, когда нам требуется отобразить несколько таблиц из базы данных на один сущностный класс модели. Такой подход обычно используется при работе с Code-Second, когда нам необходимо описать несколько таблиц, связанных по смыслу, в одном классе. В контексте нашего примера это означает, что если в базе данных имеется две таблицы Customers и Photo, то их можно отобразить на один сущностный класс Customer, содержащий данные как заказчика, так и его фотографии.

Для реализации такой модели необходимо указать Code-First наборы свойств в классе, которые должны отображаться на соответствующие таблицы. Очевидно, что сделать это с помощью аннотаций данных нельзя, т.к. аннотации в виде атрибутов нельзя указывать к подмножествам свойств в модели. Fluent API имеет специальный метод Map(), который позволяет построить список нужных свойств и указать имя таблицы.

Давайте создадим модель с классом Customer, который содержит информацию как о пользователе, так и о его фотографии:

В следующем примере показана настройка конфигурации множественных таблиц с использованием Fluent API:

Метод Map() вызывается на объекте конфигурации сущностного класса, который возвращается вызовом типизированного метода DbModelBuilder.Entity(). Этому методу передается делегат Action типизированный классом EntityMappingConfiguration, в котором имеется метод Properties(), где указывается набор свойств класса модели, который должен быть вставлен в таблицу. Во втором вызове метода Map() описывается структура таблицы Photo. Порядок вызовов методов Map() важен, т.к. при первом вызове определяется главная таблица, а далее определяются зависимые от нее таблицы. Убедиться в этом можно, взглянув на рисунок, где показана структура созданных таблиц Customer и Photo:

Создание двух таблиц из одного класса модели

Таблица Photo здесь является зависимой, т.к. она содержит внешний ключ. Между таблицами создается отношение один-к-одному. Если вы покопаетесь в свойствах таблицы Photo то увидите, что она не поддерживает каскадного удаления данных. Но Entity Framework знает, что если вы удалите покупателя из таблицы Customer, он должен будет построить команду DELETE, которая охватит обе таблицы.

При выборке данных выбирать нужно ровно столько сколько нужно за один раз. Никогда не извлекайте все данные из таблицы!


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


Вау, вау, вау, разогнался.

Самое время немного освежить знания по методам LINQ.

Давайте рассмотрим отличия между ToList AsEnumerable AsQueryable

Итак, ToList

  • Выполняет запрос немедленно.
  • Используйте .ToList() для форсирования получения данных и выхода из режима поздней загрузки (lazy loading), так что этот метод полезен перед тем как вы пройдетесь по данным.
  • Выполнение с задержкой (lazy loading)
  • Принимает параметр: Func <TSource, bool>
  • Загружает каждую запись в память приложения и управляет фильтрует его (в том числе Where/Take/Skip приведут к тому, что, например запрос select * from Table1,
  • загрузит результирующий набор в память, затем выберет первые N элементов)
  • В этом случает отрабатывает схема: Linq-to-SQL + Linq-to-Object.
  • Используйте IEnumerable для получения списка из базы данных в режиме поздней загрузки (lazy loading).
  • Выполнение с задержкой (lazy loading)
  • Преобразует Expression в T-SQL (с учетом специфики провайдера), удаленное исполняет запрос и возвращает результат в память приложения.
  • Вот почему DbSet (в Entity Framework) также наследуется от AsQueryable чтобы получать эффективные запросы.
  • Не загружает каждую запись, например если Take(5) это сгенерирует запрос вида «select top 5 * SQL» в фоновом режиме. Это означает, что этот подход более дружественный для SQL базы данных, и дает более скоростной результат.Так что AsQueryable() обычно работает быстрее, чем AsEnumerable() так как сначала генерирует T-SQL включающий в себя все условия Linq определённые вами.
  • Используйте AsQueryable если хотите запрос к базе данных который может быть улучшен перед запуском на стороне сервера.

Пример использования AsQueryable в простейшеем случае:

Волшебство простого чтения

Если вам не нужно менять данные, только отобразить используйте .AsNoTracking() метод.

Медленная выборка


Быстрая выборка (только на чтение)


Чувствую, вы немного уже размялись?

Типы загрузки связанных данных

Для тех, кто забыл, что такое lazy loading.

Ленивая загрузка (Lazy loading) означает, что связанные данные прозрачно загружаются из базы данных при обращении к свойству навигации. Подробнее читаем тут .

И заодно, напомню о других типах загрузки связанных данных.

Активная загрузка (Eager loading) означает, что связанные данные загружаются из базы данных как часть первоначального запроса.


Внимание! Начиная с версии EF Core 3.0.0, каждое Include будет вызывать добавление дополнительного JOIN к запросам SQL, создаваемым реляционными поставщиками, тогда как предыдущие версии генерировали дополнительные запросы SQL. Это может значительно изменить производительность ваших запросов, в лучшую или в худшую сторону. В частности, запросы LINQ с чрезвычайно большим числом операторов включения могут быть разбиты на несколько отдельных запросов LINQ.

Явная загрузка (Explicit loading) означает, что связанные данные явно загружаются из базы данных позднее.


Рывок и прорыв! Двигаемся дальше?

Готовы ускориться еще больше?

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

(1) Индексированное представление в контексте MS SQL Server

Индексированное представление имеет уникальный кластеризованный индекс. Уникальный кластерный индекс хранится в SQL Server и обновляется, как и любой другой кластерный индекс. Индексированное представление является более значительным по сравнению со стандартными представлениями, которые включают сложную обработку большого количества строк, например, агрегирование большого количества данных или объединение множества строк.

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

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

(2) Если нужно сделать запрос, требующий отображения более трех уровней связанных таблиц в количестве три и более c повышенной CRUD нагрузкой, лучшим способом будет задуматься о том, чтобы периодически вычислять результирующий набор, сохранять его в таблице и использовать для отображения. Результирующая таблица, в которой будут сохраняться данные должна иметь Primary Key и индексы по полям поиска в LINQ.

Что насчет асинхронности?

Да! Используем ее где только можно! Вот пример:


И да, ничего не забыли для повышения производительности? Бууум!

Внимание: метод Do() добавлен для демонстрационных целей только, с целью указать работоспособность метода GetFederalDistrictsAsync(). Как правильно заметили мои коллеги тутнужен другой пример чистой асинхронности.

Напомню, когда выполняются запросы в Entity Framework Core.

При вызове операторов LINQ вы просто создаете представление запроса в памяти. Запрос отправляется в базу данных только после обработки результатов.

Ниже приведены наиболее распространенные операции, которые приводят к отправке запроса в базу данных.

  • Итерация результатов в цикле for.
  • Использование оператора, например ToList, ToArray, Single, Count.
  • Привязка данных результатов запроса к пользовательскому интерфейсу.

Как же организовать код EF Core с точки зрения архитектуры приложения?

(1) C точки зрения архитектуры приложения, нужно обеспечить чтобы код доступа к вашей базе данных был изолирован / отделен в четко определенном месте (в изоляции). Это позволяет найти код базы данных, который влияет на производительность.

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

Как правильно и быстро сохранять данные с помощью SaveChanges?

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

Неправильно


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

Триггеры, вычисляемые поля, пользовательские функции и EF Core

Для снижения нагрузки на приложения содержащим EF Core имеет смысл применять простые вычисляемые поля и триггеры баз данных, но лучше этим не увлекаться, так как приложение может оказаться очень запутанным. А вот пользовательские функции могут быть очень полезны особенно при операциях выборки!

Параллелизм в EF Core

Если ты хочешь все запараллелить чтобы ускориться, то обломись: EF Core не поддерживает выполнение нескольких параллельных операций в одном экземпляре контекста. Следует подождать завершения одной операции, прежде чем запускать следующую. Для этого обычно нужно указать ключевое слово await в каждой асинхронной операции.

EF Core использует асинхронные запросы, которые позволяют избежать блокирования потока при выполнении запроса в базе данных. Асинхронные запросы важны для обеспечения быстрого отклика пользовательского интерфейса в толстых клиентах. Они могут также увеличить пропускную способность в веб-приложении, где можно высвободить поток для обработки других запросов. Вот пример:

А что вы знаете про компилированные запросы LINQ?

Если у вас есть приложение, которое многократно выполняет структурно похожие запросы в Entity Framework, вы часто можете повысить производительность, компилируя запрос один раз и выполняя его несколько раз с различными параметрами. Например, приложению может потребоваться получить всех клиентов в определенном городе; город указывается во время выполнения пользователем в форме. LINQ to Entities поддерживает использование для этой цели скомпилированных запросов.

Много примеров можно посмотреть тут.

Не делайте больших контекстов DbContext!

В общем так, я знаю многие из вас, если не почти все — lazy f_u__c_k__e_r__s и всю базу данных вы размещаете в один контекст, особенно это свойственно для подхода Database-First. И зря вы это делаете! Ниже приведен пример как можно разделить контекст. Конечно, таблицы соединения между контекстами придется дублировать, это минус. Так или иначе если у вас в контексте более 50 таблиц лучше подумать о его разделении.

Использование группировки контекста (pooling DdContext)

Как избежать лишних ошибок при CRUD в EF Core?

Никогда не делайте вычисления в вставку в одном коде. Всегда разделяйте формирование/подготовку объекта и его вставку/обновление. Просто разнесите по функциям: проверку введенных данным пользователем, вычисления необходимые предварительных данных, картирование или создание объекта, и собственно CRUD операцию.

Что делать, когда совсем дела плохо с производительностью приложения?

Пиво тут точно не поможет. А вот что поможет, так это разделение чтение и записи в архитектуре приложения с последующего разнесением по сокетам этих операций. Задумайтесь об использовании Command and Query Responsibility Segregation (CQRS) pattern, а также попробуйте, разделить таблицы на вставку и чтение между двумя базами данных.

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

Вы уже видели ранее использование некоторых связей между таблицами. Например, при рассмотрении примера в статье “Использование Code-First” мы создали следующую модель данных:

Code-First видит в этом примере, что вы определили навигационное свойство Orders в таблице Customer, ссылающееся на коллекцию объектов Order, что говорит о создании отношения один-ко-многим (one-to-many) между этими таблицами. Так же Code-First определит автоматически эту связь, создав внешний ключ для таблицы Order, привязанный к первичному ключу CustomerId таблицы Customer.

Далее мы опишем все соглашения, которые используются в Code-First для описания связей между таблицами.

Использование навигационных свойств

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

Отношения между таблицами можно определить в модели только за счет использования навигационных свойств, которые могут быть указаны в обеих таблицах (двусторонняя связь, как показано в примере выше) или в одной таблице (односторонняя связь). Ниже описаны некоторые соглашения Code-First при использовании навигационных свойств:

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

Если в обеих таблицах навигационные свойства имеют тип коллекций, то Code-First предполагает наличие связи между ними многие-ко-многим (many-to-many).

Если в обеих таблицах навигационные свойства представлены в виде ссылок друг на друга, то Code-First предполагает отношение между таблицами один-к-одному (one-to-one).

В случае реализации отношения один-к-одному, вы должны будете предоставить некоторую дополнительную информацию, чтобы Code-First знал, какая сущность является основной, а какая зависимой. Если в таблицах явно не указан внешний ключ, то Code-First смоделирует отношение один-или-ноль-к-одному (zero-or-one-to-one, 0..1-1), т.е. добавление данных в главную таблицу, необязательно должно вести к добавлению данных в зависимую таблицу.

Посмотрев пример нашей модели, показанной выше, можно проследить использование этих соглашений на классах Customer и Order. Например, Code-First автоматически создаст отношение между таблицами один-ко-многим, т.к. мы использовали тип коллекции в одной таблице и простую ссылку в другой. Также можно догадаться, что в отношении этих таблиц Customer будет главной, а Order зависимой, т.е. мы можем вставить данные заказчика в таблицу Customer, не добавляя при этом для него заказы. И наоборот, в таблицу Order мы можем вставить только заказ, привязанный к конкретному покупателю.

Стоит также сказать, что без явного указания внешних ключей в коде модели, Code-First будет генерировать эти ключи автоматически, используя имя, сочетающие в себе название базовой таблицы и первичного ключа этой таблицы. Например, для таблицы Order будет сгенерирован внешний ключ Customer_CustomerId.

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

Если вы запустите приложение и обновите структуру базы данных, то обнаружите, что Code-First изменил тип внешнего ключа Customer_CustomerId – теперь он не может поддерживать значения NULL:

Отключение поддержки NULL для внешнего ключа с помощью аннотаций данных

Напомню, что мы используем в качестве примера приложение, созданное в статье “Использование Code-First” и для воссоздания базы данных при изменении модели требуется либо ее ручное удаление всякий раз, когда база данных изменилась, либо использование настроек Code-First по автоматическому обнаружению изменений в модели.

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

Параметр Multiplicity в этом шаблоне указывает на окончание используемых методов Has… и With…, он может иметь следующие значения: Optional (навигационное свойство может иметь один или ноль экземпляров), Required (навигационное свойство может иметь только один экземпляр) и Many (навигационное свойство содержит коллекцию экземпляров).

Соответственно Entity Framework определяет следующий набор методов, определяющих настройки первичных навигационных свойств:

Для настройки отношений во второй таблице, используются следующие методы:

При вызове этих методов в качестве параметра, им передается делегат, в котором указывается навигационное свойство. При использовании односторонней связи (когда в одной из таблиц отсутствует навигационное свойство), можно вызвать соответствующий метод без параметров. Ниже показан пример настройки навигационных свойств для наших таблиц, который соответствует автоматическим соглашениям Code-First. Т.е. фактически он создает связь один-ко-многим между нашими таблицами, где таблица Customer является главной:

Ранее мы показали, как с помощью аннотаций данных можно ограничить поддержку NULL значений для автоматически сгенерированного внешнего ключа. Давайте реализуем это с помощью Fluent API:

В этом примере мы просто поменяли вызов метода WithOptional() на WithRequired(). Если вы запустите пример и посмотрите на структуру таблицы Orders, то увидите, что ее структура аналогична той, которая показана на первом рисунке в статье, когда мы использовали атрибуты метаданных. На рисунке ниже наглядно показано, как выполняется этот запрос:

Выполнение запроса Fluent API для определения отношений между таблицами

Указание внешних ключей

Ранее мы рассмотрели, как реализовать отношения между таблицами без прямого использования внешних ключей. Например, класс Order содержит свойство-ссылку на класс Customer, но при этом в этом классе не определено свойство, которое будет использоваться в качестве внешнего ключа для связи между таблицами. В этом случае мы видели, что Code-First автоматически сгенерирует внешний ключ за вас. Теперь давайте рассмотрим, что происходит, если мы явно задаем внешний ключ.

Первое что мы сделаем, это добавим новое свойство CustomerId в класс модели Order:

Запустите приложение. Code-First поймет, что вы внесли изменения в модель и воссоздаст базу данных. Если вы рассмотрите структуру столбцов таблицы Order, то заметите, что Code-First автоматически распознал поле CustomerId как внешний ключ и заменил автоматически генерируемый ключ Customer_CustomerId на CustomerId:

Явное указание внешнего ключа для таблицы

Как вы понимаете, Code-First использует определенные соглашения при поиске внешнего ключа в свойствах модели. Эти правила основаны на имени свойства и придерживаются следующих шаблонов:

В нашем случае имя свойства CustomerId подходит под первое правило, т.к. в главной таблице Customer используется одноименное свойство, которое является первичным ключом таблицы. Также стоит отметить, что Code-First не чувствителен к регистру символов при поиске внешнего ключа, т.е. если бы в таблице Order у нас было бы свойство CusToMeRId, то Code-First автоматически бы распознал его как внешний ключ.

К данному моменту может возникнуть вопрос, зачем может понадобиться явное определение внешних ключей в классе модели, если Code-First способен автоматически создавать эти ключи? Ответом на этот вопрос будет то, что иногда гораздо удобней получить доступ к родительскому объекту в коде через внешний ключ, нежели чем через ссылку. Например, в коде вы могли бы создать новый объект Order и указать через ссылку объект Customer, к которому он должен принадлежать:

Очевидно, для того, чтобы загрузить объект Customer в экземпляр myCustomer, вам необходимо будет сначала обратиться к базе данных. Использование внешнего ключа позволяет просто указать идентификатор заказчика не ссылаясь на него. Чтобы получить идентификатор заказчика, зачастую нужно также обратиться к базе данных, но бывают случаи, когда у вас есть доступ к значению ключа этого объекта. Например, если мы знаем что заказ myOrder принадлежит заказчику с идентификатором 5, мы могли бы использовать внешний ключ вместо ссылки на объект:

Кроме того, при использовании ссылки иногда возникает более серьезная ошибка. Entity Framework отслеживает состояние объектов сущностных классов и при их изменении помечает объект, изменяя свойство DbEntityEntry.State. Если вы создадите новый объект myOrder, укажите в нем ссылку на уже существующий в памяти объект myCustomer и попытаетесь сохранить объект myCustomer в базе данных, то EF пометит состояние этого объекта как Added, а не Modified, т.к. в коде изменился список заказов, связанных с этим покупателем и EF предполагает, что был создан новый покупатель. В результате в таблицу будет добавлен новый заказчик, хотя предполагалось просто добавить заказ для уже существующего заказчика. Эту проблему можно избежать либо сохранив в базе данных только объект myOrder, либо используя внешний ключ.

Есть еще один момент, который нужно упомянуть при обсуждении соглашений Code-First по внешним ключам. Когда ранее мы не использовали внешних ключей, Code-First автоматически генерировал ключ, который поддерживал значения NULL. Если вы взгляните на рисунок выше, то увидите, что внешний ключ CustomerId в таблице Order имеет не обнуляемый тип NOT NULL, поэтому вы не сможете сохранить новый заказ не указав идентификатор покупателя (используя внешний ключ или ссылку). Тем не менее, можно явно указать, что внешний ключ должен поддерживать значения NULL. Для этого измените тип свойства CustomerId в классе модели Order на обнуляемый тип данных:

Итак, из всего сказанного выше, можно сделать вывод, что Code First позволяет определять отношения без использования внешних ключей в ваших классах. Тем не менее, разработчики иногда сталкиваются с некоторыми запутанными ошибками, когда работают с моделями, не имеющими внешних ключей, как было показано ранее.

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

Иногда может возникнуть вопрос, что происходит, если имя вашего внешнего ключа не соответствует соглашениям Code-First? Например, вы могли бы использовать в таблице Order внешний ключ с именем UserId, как показано ниже:

Если вы запустите приложение и воссоздадите базу данных, то увидите, что Code-First проигнорировал поле UserId и создал автоматически генерируемый внешний ключ Customer_CustomerId, а поле UserId было добавлено как обычный столбец. Вы можете решить эту проблему используя атрибут ForeignKey в классе модели данных, как показано в примере:

В конструкторе этого атрибута указывается имя навигационного свойства, если оно имеется в классе модели. Альтернативным способом является применение атрибута ForeignKey к навигационному свойству:

В данном случае в конструкторе указывается имя свойства, являющегося внешним ключом. В Fluent API используется специальный метод HasForeignKey(), как показано в примере ниже:

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

Работа с обратными навигационными свойствами

Пока мы использовали по одному навигационному свойству между двумя классами модели, Code-First понимал, как настроить отношения между ними. Существует такие случаи, когда между двумя таблицами базы данных нужно определить несколько отношений. Например, таблица Customers могла бы ссылаться на все заказы, на обработанные заказы (которые оплатил покупатель) и необработанные заказы. Логичнее всего решить данную проблему, это просто добавить новый столбец, например IsProcess, в таблицу Orders, который имел бы логическое значение и указывал бы на то, обработан заказ или нет. Но также эту проблему можно решить использовав три внешних ключа, связывающих эти таблицы.

В модели классов это решение будет выглядеть следующим образом:

В данном примере Code-First не сможет автоматически распознать связь между навигационными свойствами этих классов. Если вы выполните этот пример, то увидите, что в созданной таблице Orders было добавлено пять внешних ключей – по одному для каждого несвязанного навигационного свойства, и один ключ для связанных свойств Orders и Customer (если вы удалите настройку Fluent API, показанную ранее, в которой мы привязали эти свойства и указали внешний ключ, то Code-First сгенерирует 6 внешних ключей).

Отсутствие связи между навигационными свойствами

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

Теперь будет создано три внешних ключа, как и требовалось:

Настройка нескольких отношений между таблицами

Использование однонаправленной связи между таблицами

В предыдущих примерах мы рассмотрели способы создания двунаправленных связей между таблицами, т.е. когда в обоих классах модели определяется навигационное свойство. Тем не менее определение пары навигационных свойств не является обязательным при работе с Entity Framework. В нашей модели мы можем указать ссылку только в одном классе, например, в классе Customer оставить ссылку на коллекцию объектов Order, а в классе Order удалить ссылку на Customer:

В этом примере мы не стали удалять внешний ключ CustomerId, благодаря чему, Entity Framework четко определяет связь между таблицами с использованием этого ключа, используя соглашения, описанные выше. Теперь давайте сделаем еще один шаг и удалим внешний ключ из таблицы Order:

Ранее мы уже сказали, что по соглашению Code-First сгенерирует автоматически внешний ключ, если он не объявлен явно в классе модели. Это же соглашение работает, если мы используем однонаправленную связь между таблицами. Класс Customer по прежнему имеет навигационное свойство, определяющие его отношение с Order, поэтому будет генерироваться внешний ключ с именем Customer_CustomerId в таблице Orders.

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

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

Как мы описывали раньше, чтобы явно указать классу Order, что UserId является внешним ключом, можно использовать атрибут ForeignKey и передать ему имя навигационного свойства в параметре. Что делать, если мы используем одностороннюю связь и в классе Order не используем навигационное свойство?

Для решения этой проблемы мы можем использовать этот атрибут в главной таблице к навигационному свойству Orders или использовать Fluent API, как показано в примере ниже:

Обратите внимание, что при использовании Fluent API в вызове метода WithRequired() мы не передаем параметр делегата с выбором навигационного свойства из модели, т.к. используем одностороннюю связь и у нас отсутствует навигационное свойство в классе Order.


Взаимосвязи EF Core - концепции и свойства навигации

В настоящее время у нас есть только один класс сущности (модели), класс Student , но довольно скоро мы собираемся создать остальную часть модели базы данных в нашем приложении. Но прежде чем мы это сделаем, очень важно понять некоторые основные концепции при работе с реляционными базами данных и моделями.

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

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

Кроме того, давайте объясним отношения Required и Optional в EF Core. Обязательная связь - это связь, в которой внешний ключ не может быть нулевым. Это означает, что должен существовать главный объект. Необязательное отношение - это отношение, в котором внешний ключ может иметь значение NULL и, следовательно, основной объект может отсутствовать.

Настройка One-to-One связи

Отношение "один к одному" означает, что строка в одной таблице может относиться только к одной строке в другой таблице в связи. Это не такая распространенная связь, потому что она обычно обрабатывается как «все данные в одной таблице», но иногда (когда мы хотим разделить наши сущности) полезно разделить данные на две таблицы.

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

Теперь, чтобы установить связь между классами Student и StudentDetails , нам нужно добавить свойство навигации по ссылкам с обеих сторон. Итак, давайте сначала изменим класс Student :

И давайте изменим класс StudentDetails :

Мы можем видеть, что класс Student имеет свойство навигации по ссылке к классу StudentDetails , а класс StudentDetails имеет внешний ключ и свойство навигации Student .

В результате мы можем создать новую миграцию и применить ее:

Настройка One-to-One связи EF Core

Отлично, отлично работает.

Дополнительные пояснения

Как мы объяснили в первой статье, EF Core ищет все общедоступные свойства DbSet<T> в классе DbContext для создания таблиц в базе данных. Затем он ищет все общедоступные свойства в классе T для сопоставления столбцов. Но он также выполняет поиск всех общедоступных свойств навигации в классе T и создает дополнительные таблицы и столбцы, связанные с типом свойства навигации. Итак, в нашем примере в классе Student EF Core находит свойство навигации StudentDetails и создает дополнительную таблицу со своими столбцами.

Конфигурация отношений One-to-Many

В этом разделе мы узнаем, как создавать отношения "один ко многим" всеми тремя способами. Итак, прежде чем мы начнем, давайте создадим дополнительный класс модели Evaluation в проекте Entities :

Использование условного подхода для создания отношений «один ко многим»

Давайте посмотрим на различные соглашения, которые автоматически настраивают связь "один ко многим" между классами Student и Evaluation .

Первый подход включает свойство навигации в основной сущности, классе Student :

В классе ApplicationContext есть свойство DbSet , и, как мы объяснили, EF Core выполняет поиск по классу Student , чтобы найти все свойства навигации для создания соответствующих таблиц в базе данных.

Еще один способ создать связь "один ко многим" - это добавить свойство Student в класс Evaluation без свойства ICollection в классе Student класс :

Чтобы этот подход работал, мы должны добавить свойство DbSet<Evaluation> Evaluations в класс ApplicationContext .

Третий подход по Конвенции заключается в использовании комбинации предыдущих. Итак, мы можем добавить свойство навигации ICollection<Evaluation> Evaluations в класс Student и добавить свойство навигации Student Student в класс Evaluation . Конечно, при таком подходе нам не нужно свойство DbSet<Evaluation> Evaluations в классе ApplicationContext .

Это результат любого из этих трех подходов:

Настройка связей сущностей в EF Core

Мы видим, что связь была создана правильно, но наш внешний ключ является полем, допускающим значение NULL. Это связано с тем, что оба свойства навигации имеют значение по умолчанию null. Это отношение также называется необязательным отношением (мы говорили об этом в первой части этой статьи).

Если мы хотим создать требуемую связь между сущностями Student и Evaluation , мы должны включить внешний ключ в класс Evaluation . :

Теперь, когда мы выполним нашу миграцию, мы увидим следующий результат:

Миграции EF Core

Очевидно, что наши отношения сейчас необходимы.

Подход с аннотациями данных

Подход с использованием аннотаций к данным содержит только два атрибута, связанных с отношениями. Атрибуты [ForeignKey] и [InverseProperty] .

Атрибут [ForeignKey] позволяет нам определять внешний ключ для свойства навигации в классе модели. Итак, давайте изменим класс Evaluation , добавив этот атрибут:

Мы применили атрибут [ForeignKey] поверх свойства StudentId (которое является внешним ключом в этом классе), присвоив ему имя свойства навигации Student . Но работает и наоборот:

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

[ForeignKey («Свойство1», «Свойство2»)] .

Какой бы способ мы ни выбрали, результат будет таким же, как и при подходе «по соглашению». Мы собираемся создать требуемую связь между этими двумя таблицами:

Установление связей меду таблицами в EF Core

Подход Fluent API для конфигурации One-to-Many

Чтобы создать отношение «один ко многим» с этим подходом, нам нужно удалить атрибут [ForeignKey] из класса Evaluation и изменить StudentConfiguration , добавив этот код:

С помощью такого кода мы сообщаем EF Core, что наша сущность Student (объект построителя имеет тип) могут быть связаны со многими объектами Evaluation . Мы также заявляем, что Evaluation находится во взаимосвязи только с одной сущностью Student . Наконец, мы предоставляем информацию о внешнем ключе в этой связи.

Результат будет таким же:

One-to-Many связь в EF Core

Здесь нужно упомянуть одну вещь.

Для модели базы данных, такой как мы определили, нам не нужен метод HasForeignKey . Это потому, что свойство внешнего ключа в классе Evaluation имеет тот же тип и то же имя, что и первичный ключ в классе Student. Это означает, что по Конвенции это отношение все равно будет обязательным. Но если бы у нас был внешний ключ с другим именем, например StudId, тогда понадобился бы метод HasForeignKey , потому что в противном случае ядро EF создало бы необязательную связь между классами Evaluation и Student.

Конфигурация отношений Many-to-Many (многие-ко-многим)

Это реализация версии 3.1 EF Core. Это справедливо для EF Core версии 5, но в версии 5 это можно было бы сделать немного иначе. Мы объясним это в следующем разделе.

Прежде чем мы начнем объяснять, как настроить эту связь, давайте создадим необходимые классы в проекте Entities :

Теперь мы можем изменить классы Student и Subject , предоставив свойство навигации для каждого класса по направлению к классу StudentSubject :

В Entity Framework Core мы должны создать объединяющуюся сущность для объединяемой таблицы (StudentSubject). Этот класс содержит внешние ключи и свойства навигации из классов Student и Subject . Кроме того, классы Student и Subject имеют свойства навигации ICollection по отношению к классу StudentSubject . Таким образом, отношения «многие ко многим» - это всего лишь два отношения «один ко многим».

Мы создали наши сущности, и теперь нам нужно создать необходимую конфигурацию. Для этого давайте создадим класс StudentSubjectConfiguration в папке Entities/Configuration:

Как мы уже говорили, многие-ко-многим - это всего лишь две взаимосвязи EF Core «один ко многим», и это именно то, что мы настраиваем в нашем коде. Мы создаем первичный ключ для таблицы StudentSubject , который в данном случае является составным ключом. После настройки первичного ключа мы используем знакомый код для создания конфигураций отношений.

Теперь нам нужно изменить метод OnModelBuilder в классе ApplicationContext :

После этих изменений мы можем создать миграцию и применить ее:

PM> Add-Migration ManyToManyRelationship

Связь Many-to-Many в EF Core

Отличная работа. Давай продолжаем.

По сути, класс Student должен иметь public ICollection Subjects , а класс Subject должен иметь public ICollection Students свойство. Нет необходимости ни в третьем классе, ни в свойствах навигации для этого класса.

Но если вы хотите изначально заполнить данные для таблиц Student и Subject и заполнить третью таблицу идентификаторами обоих таблиц, вам придется использовать реализацию, которую мы использовали для версии 3.1.

Метод OnDelete

Метод OnDelete настраивает действия удаления между реляционными сущностями. Мы можем добавить этот метод в конец конфигурации отношений, чтобы решить, как будут выполняться действия удаления.

В методе OnDelete можно использовать следующие значения:

  • Restrict - действие удаления не применяется к зависимым объектам. Это означает, что мы не можем удалить основную сущность, если у нее есть связанная зависимая сущность.
  • SetNull - зависимая сущность не удаляется, но для ее свойства внешнего ключа установлено значение null.
  • ClientSetNull - если EF Core отслеживает зависимую сущность, ее внешний ключ имеет значение null, и эта сущность не удаляется. Если он не отслеживает зависимую сущность, то применяются правила базы данных.
  • Cascade - зависимая сущность удаляется вместе с основной сущностью.

Мы также можем видеть это из кода в нашем файле миграции:

onDelete в миграциях EF Core

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

Давайте создадим еще один перенос:

PM> Добавление миграции StudentEvaluationRestrictDelete

И взгляните на сгенерированный код миграции:

Миграции FluentApi с onDelete в EF Core

Заключение

Настройка взаимосвязей EF Core в нашей модели базы данных - очень важная часть процесса моделирования.

Мы увидели, что EF Core предлагает нам несколько способов добиться этого и максимально упростить процесс.

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

Читайте также: