Entity framework настройка связей

Обновлено: 08.07.2024

В предыдущей статье вы узнали о соглашениях 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.

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

Большинство примеров в этой статье используют связь «один ко многим» для демонстрации концепций. Примеры связей "один к одному" и "многие ко многим" см. в разделе другие шаблоны связей в конце статьи.

Определение терминов

Существует ряд терминов, используемых для описания связей.

Зависимая сущность: Это сущность, содержащая свойства внешнего ключа. Иногда называется "дочерним" отношением.

Сущность субъекта: Это сущность, содержащая свойства первичного или альтернативного ключа. Иногда называется "родителем" связи.

Основной ключ: Свойства, однозначно идентифицирующие сущность Principal. Это может быть первичный ключ или альтернативный ключ.

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

Свойство навигации: Свойство, определенное для основной и (или) зависимой сущности, ссылающейся на связанную сущность.

Свойство навигации коллекции: Свойство навигации, содержащее ссылки на множество связанных сущностей.

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

Обратное свойство навигации: При обсуждении определенного свойства навигации этот термин относится к свойству навигации на другом конце связи.

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

В следующем коде показана связь «один ко многим» между Blog и Post

Post является зависимой сущностью

Blog является основной сущностью

Blog.BlogId является основным ключом (в данном случае это первичный ключ, а не альтернативный ключ);

Post.BlogId является внешним ключом

Post.Blog является свойством навигации по ссылке

Blog.Posts — Это свойство навигации коллекции

Post.Blog Свойство обратной навигации Blog.Posts (и наоборот)

Соглашения

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

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

Полностью определенные связи

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

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

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

  • <navigation property name><principal key property name>
  • <navigation property name>Id
  • <principal entity name><principal key property name>
  • <principal entity name>Id

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

Если свойство является первичным ключом или имеет тип, несовместимый с ключом субъекта, он не будет настроен в качестве внешнего ключа.

До EF Core 3,0 свойство с именем, точно совпадающее со свойством ключа субъекта, также было сопоставлено с внешним ключом .

Нет свойства внешнего ключа

Хотя рекомендуется использовать свойство внешнего ключа, определенное в зависимом классе сущности, оно не является обязательным. Если свойство внешнего ключа не найдено, свойство теневого внешнего ключа будет вводиться с именем или, <principal entity name><principal key property name> Если в зависимом типе отсутствует Навигация.

В этом примере теневым внешним ключом является то, BlogId что при ожидании имя навигации будет избыточным.

Если свойство с таким именем уже существует, то имя теневого свойства будет суффиксом с номером.

Одно свойство навигации

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

Ограничения

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

Каскадное удаление

По соглашению каскадное удаление будет установлено в CASCADE для требуемых связей и клиентсетнулл для необязательных связей. Cascade средства также удаляются из зависимых сущностей. ClientSetNull означает, что зависимые сущности, не загруженные в память, останутся неизменными и должны быть удалены вручную или обновлены для указания на действительную сущность Principal. Для сущностей, загруженных в память, EF Core попытается установить свойства внешнего ключа в значение null.

Различия между обязательными и дополнительными связями см. в разделе обязательные и дополнительные связи .

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

Настройка вручную

чтобы настроить связь в Fluent API, начните с определения свойств навигации, составляющих связь. хасоне или HasMany определяет свойство навигации для типа сущности, на котором начинается настройка. Затем вы позвоните в цепочку WithOne или WithMany для поиска обратной навигации. хасоне / WithOne используются для свойств навигации по ссылке и HasMany / WithMany используются для свойств навигации по коллекциям.

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

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

Одно свойство навигации

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

Настройка свойств навигации

Эта возможность появилась в EF Core 5.0.

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

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

Внешний ключ

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

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

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

[ForeignKey] Заметку можно поместить в любое свойство навигации в связи. Не требуется переходить к свойству навигации в классе зависимой сущности.

Свойство, указанное с помощью [ForeignKey] в свойстве навигации, не обязательно должно существовать в зависимом типе. В этом случае для создания теневого внешнего ключа будет использоваться указанное имя.

Теневой внешний ключ

HasForeignKey(. ) Чтобы настроить свойство теневого копирования в качестве внешнего ключа, можно использовать строку перегрузки в. Дополнительные сведения см. в разделе HasForeignKey(. ) . Рекомендуется явно добавить свойство Shadow в модель, прежде чем использовать ее как внешний ключ (как показано ниже).

Имя ограничения внешнего ключа

В соответствии с соглашением, при нацеливании на реляционную базу данных ограничения внешнего ключа именуются FK_ < зависимого типа имя > _ < участника Name _ имя >< Свойства внешнего ключа > . Для составных внешних ключей < имя свойства внешнего ключа > преобразуется в список имен свойств внешнего ключа с разделителями подчеркивания.

Имя ограничения также можно настроить следующим образом:

Без свойства навигации

Указывать свойство навигации не обязательно. Можно просто предоставить внешний ключ на одной стороне связи.

Основной ключ

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

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

Обязательные и необязательные связи

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

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

Вызов IsRequired(false) также делает свойство внешнего ключа необязательным, если оно не настроено в противном случае.

Каскадное удаление

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

Подробное описание каждого варианта см. в разделе каскадное удаление .

Другие шаблоны отношений

Один к одному

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

при настройке связи с помощью Fluent API используются HasOne WithOne методы и.

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

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

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

Возможность настройки того, является ли зависимый объект обязательным, появился в EF Core 5,0.

"Много ко многим"

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

Способ реализации этой связи в базе данных заключается в таблице JOIN, содержащей внешние ключи и в, и в Post Tag . Например, это то, что EF будет создаваться в реляционной базе данных для указанной выше модели.

На внутреннем уровне EF создает тип сущности для представления соединяемой таблицы, которая будет называться типом сущности JOIN. Словарь<строка, объект> в настоящее время используется для управления любым сочетанием свойств внешнего ключа. Дополнительные сведения см. в разделе типы сущностей контейнера свойств . В модели может существовать несколько связей "многие-ко-многим", поэтому в этом случае типу сущности JOIN должно быть присвоено уникальное имя PostTag . Функция, которая разрешает это, называется типом сущности общего типа.

Тип CLR, используемый для объединения типов сущностей по соглашению, может измениться в будущих выпусках для повышения производительности. Не зависят от типа объединения, Dictionary<string, object> если он не был явно настроен, как описано в следующем разделе.

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

Соединение конфигурации типа сущности

Обычно конфигурация применяется к типу сущности JOIN. Это действие можно выполнить с помощью UsingEntity .

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

Дополнительные данные могут храниться в типе сущности JOIN, но для этого лучше создать собственный тип CLR. При настройке связи с типом сущности пользовательского объединения необходимо явно указать оба внешних ключа.

Настройка связей соединения

EF использует связи 2 1-ко-многим в типе сущности Join для представления связи «многие ко многим». Эти связи можно настроить в UsingEntity аргументах.

Возможность настройки связей "многие ко многим" появилась в EF Core 5,0, для предыдущей версии используется следующий подход.

Косвенные связи "многие ко многим"

Можно также представить связь «многие ко многим», просто добавив тип сущности «соединение» и сопоставив две отдельные связи «один ко многим».

Поддержка формирования шаблонов для связей "многие ко многим" из базы данных еще не добавлена. Отслеживайте решение этого вопроса здесь.

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

Связи в EF

В реляционных базах данных связи (также называемые связями) между таблицами определяются через внешние ключи. Внешний ключ (FK) — это столбец или сочетание столбцов, которое применяется для принудительного установления связи между данными в двух таблицах. Обычно существует три типа связей: один к одному, один ко многим и многие ко многим. В связи «один ко многим» внешний ключ определяется в таблице, которая представляет собой множество элементов связи. Связь «многие ко многим» включает определение третьей таблицы (которая называется соединением или связующей таблицей), первичный ключ которых состоит из внешних ключей из обеих связанных таблиц. В связи «один к одному» первичный ключ действует в качестве внешнего ключа и не имеет отдельного внешнего ключевого столбца для любой таблицы.

На следующем рисунке показаны две таблицы, участвующие в связи «один ко многим». Таблица Course является зависимой таблицей, так как она содержит столбец DepartmentID , связывающий его с таблицей отдела .

Таблицы отделов и курсов

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

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

Рекомендуется включать в модель свойства, которые сопоставляются с внешними ключами в базе данных. Включение свойств внешних ключей позволяет создавать или изменять отношение, изменяя значение внешнего ключа для зависимого объекта. Сопоставление такого типа называется сопоставлением на основе внешнего ключа. Использование внешних ключей еще более важно при работе с отключенными сущностями. Обратите внимание, что при работе с 1-1 или 1 на-0. 1 связи нет отдельного внешнего ключевого столбца, свойство первичного ключа выступает в качестве внешнего ключа и всегда включается в модель.

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

В модели можно использовать один или оба типа сопоставлений. Однако если у вас есть отношение "многие ко многим", которое соединено таблицей соединения, содержащей только внешние ключи, EF будет использовать независимую ассоциацию для управления связью "многие ко многим".

На следующем рисунке показана концептуальная модель, созданная с помощью Entity Framework Designer. Модель содержит две сущности, участвующие в связи "один ко многим". Обе сущности имеют свойства навигации. Курс является зависимой сущностью и имеет определенное свойство внешнего ключа DepartmentID .

Таблицы отделов и курсов со свойствами навигации

В следующем фрагменте кода показана та же модель, которая была создана с помощью Code First.

Настройка или сопоставление связей

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

  • сведения о настройке связей в Code First см. в разделе аннотации данных и Fluent API — связи.
  • Сведения о настройке связей с помощью Entity Framework Designer см. в разделе связи с конструктором EF.

Создание и изменение связей

При взаимосвязи с внешним ключомсостояние зависимого объекта с состоянием изменяется на EntityState.Modified . В независимых отношениях изменение связи не приводит к обновлению состояния зависимого объекта.

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

Путем присвоения нового значения свойству внешнего ключа, как показано в следующем примере.

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

Если ссылка находится в добавленном состоянии (в данном примере это объект Course), свойство навигации ссылки не будет синхронизировано с ключевыми значениями нового объекта до тех пор, пока не будет вызван метод SaveChanges. Синхронизация не выполняется, поскольку контекст объекта не содержит постоянных ключей для добавленных объектов, пока они не будут сохранены. Если необходимо полностью синхронизировать новые объекты, как только вы настроили связь, используйте один из следующих методов. *

С помощью присваивания нового объекта свойству навигации. Следующий код создает связь между курсом и department . Если объекты присоединены к контексту, то объект course также добавляется в department.Courses коллекцию, а соответствующее свойство внешнего ключа course объекта задается значением свойства ключа отдела.

Путем удаления или добавления объекта в коллекцию сущностей. Например, можно добавить объект типа Course в department.Courses коллекцию. Эта операция создает связь между определенным курсом и конкретным . Если объекты присоединены к контексту, ссылка на отдел и свойство внешнего ключа в объекте Course будут установлены соответствующим образом .

С помощью ChangeRelationshipState метода можно изменить состояние указанной связи между двумя объектами сущностей. Этот метод чаще всего используется при работе с N-уровневых приложениями и независимой ассоциацией (его нельзя использовать с Ассоциацией внешнего ключа). Кроме того, чтобы использовать этот метод, необходимо раскрывающийся список ObjectContext , как показано в примере ниже.
В следующем примере существует связь «многие ко многим» между преподавателями и курсами. При вызове ChangeRelationshipState метода и передаче EntityState.Added параметра сообщается SchoolContext о том, что между двумя объектами была добавлена связь.

Обратите внимание, что при обновлении (не просто добавлении) связи необходимо удалить старую связь после добавления новой.

Синхронизация изменений между внешними ключами и свойствами навигации

При изменении связи объектов, присоединенных к контексту с помощью одного из описанных выше методов, Entity Framework необходимо синхронизировать внешние ключи, ссылки и коллекции. Entity Framework автоматически управляет этой синхронизацией (также называется исправлением связи) для сущностей POCO с прокси-серверами. Дополнительные сведения см. в разделе Работа с учетными записями-посредниками.

При использовании сущностей POCO без прокси-серверов необходимо убедиться в том, что метод DetectChanges вызывается для синхронизации связанных объектов в контексте. Обратите внимание, что следующие интерфейсы API автоматически активируют вызов DetectChanges .

  • DbSet.Add
  • DbSet.AddRange
  • DbSet.Remove
  • DbSet.RemoveRange
  • DbSet.Find
  • DbSet.Local
  • DbContext.SaveChanges
  • DbSet.Attach
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries
  • Выполнение запроса LINQ к элементу DbSet

Загрузка связанных объектов

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

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

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

Управление параллелизмом

Как в внешних, так и в независимых ассоциациях проверки параллелизма основываются на ключах сущностей и других свойствах сущности, определенных в модели. При использовании конструктора EF для создания модели присвойте ConcurrencyMode атрибуту значение ConcurrencyMode , чтобы указать, что свойство должно быть проверено на наличие параллелизма. при использовании Code First для определения модели используйте ConcurrencyCheck заметку для свойств, которые необходимо проверить на наличие параллелизма. при работе с Code First можно также использовать TimeStamp заметку, чтобы указать, что свойство должно проверяться на параллелизм. В данном классе может быть только одно свойство timestamp. Code First сопоставляет это свойство с полем базы данных, не допускающим значения null.

При работе с сущностями, участвующими в проверке и разрешении параллелизма, рекомендуется всегда использовать связь по внешнему ключу.

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

Работа с перекрывающимися ключами

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

между двумя таблицами базы данных указывает на то, что для одной строки главной таблицы, обязательно определяется одна связанная строка в зависимой таблице. Это отношение имеет еще один подвид, который называется ноль-или-один-к-одному (zero-or-one-to-one). При этом отношении, в зависимой таблице необязательно указывать связанную строку, т.е. одной строке в главной таблице, может соответствовать ноль или одна строка в зависимой таблице.

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

Давайте добавим в нашу модель класс профиля покупателя Profile, который свяжем с таблицей Customer:

В этом примере изначально допущена ошибка. Code-First не сможет определить, какой класс является зависимым в этой ситуации (т.е. для какого класса нужно создать внешний ключ). Если вы запустите приложение с этой моделью, то EF сгенерирует исключение InvalidOperationException, указав, что не может определить основной класс в связке Profile-Customer.

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

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

Первичный и внешний ключ таблицы при отношении один-к-одному

Ранее, при обсуждении использования внешних ключей мы говорили, что атрибут ForeignKey можно применить не только к свойству ключа в модели, но и к навигационному свойству. Для нашей модели такой подход является ошибочным, если вы примените атрибут [ForeignKey(“CustomerId”)] к свойству CustomerOf в классе Profile, Code-First не поймет, в какой таблице нужно будет создать внешний ключ, т.к. и таблица Customer и Profile содержат поле CustomerId. Так что данный способ определения внешних ключей не подходит для нашего сценария.

На следующем рисунке показана диаграмма, на которой можно видеть отношение ноль-или-один-к-одному между таблицами Customers и Profiles:

Отношение ноль-или-один-к-одному

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

Обратите внимание, что здесь не нужно использовать метод HasForeignKey() для явного указания внешнего ключа. Этого кода на самом деле достаточно для Code-First, чтобы показать, что класс Profile является зависимым, а свойство Profile.CustomerId должно являться внешним ключом. Это связано с тем, что при определении отношения one-to-one (или zero-or-one-to-one) первичный ключ в зависимой таблице должен автоматически быть внешним ключом.

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

В результате, если мы попытаемся вставить данные нового заказчика, не указав для него профиль, то Entity Framework сгенерирует ошибку. Следует отметить, что при реализации отношения один-к-одному структура базы данных не меняется, изменения касаются только модели, которые уведомляют EF об обоюдной зависимости таблиц Customer и Profile.

Отношение один-к-одному можно создать, используя также Fluent API. Логично было бы предположить, что для этого используется вызов метода HasRequired с последующим вызовом WithRequired. Однако, Fluent API предлагает вместо вызова WithRequired использовать два следующих метода: WithRequiredDependent или WithRequiredPrincipal. Выбор нужного метода зависит от того, к какой таблице он применяется – зависимой (WithRequiredPrincipal укажет ссылку на основную таблицу) или основной (WithRequiredDependent укажет ссылку на зависимую таблицу). Чтобы в этом разобраться, достаточно взглянуть на простой пример:

Здесь важно отметить, что если вы перепутаете в этом примере вызовы этих методов, например:

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

В примерах выше мы использовали перегруженные методы WithRequiredDependent() и WithRequiredPrincipal() передавая в параметре ссылку на навигационное свойство. Однако эти методы можно указывать без параметров. Делать это следует только в том случае, если в классах таблиц, к которым они применяются, отсутствуют соответствующие навигационные свойства. Т.е. благодаря вызову этих методов без параметров, мы можем моделировать связь один-к-одному используя одностороннее отношение между классами (когда навигационное свойство указывается только в одном из классов).

Существует еще одна разновидность отношения один-к-одному, которую мы еще не рассмотрели, это отношение ноль-или-один-к-ноль-или-одному (zero-or-one-to-zero-or-one). В контексте нашего примера это отношение говорит о том, что для любого покупателя может существовать ноль или один профиль, и для любого профиля может существовать ноль или один покупатель. В модели магазина такая связь не имеет логического смысла – как может существовать профиль пользователя без пользователя? Однако, в некоторых случаях вам может понадобиться создать эту связь.

Для реализации связи ноль-или-один-к-ноль-или-одному в Fluent API существуют специальные методы WithOptionalDependent() и WithOptionalPrincipal(), смысл которых аналогичен соответствующим методам WithRequired… , но они применяются после вызова метода HasOptional(), как показано в примере ниже:

Обратите внимание, что в этом примере мы явно задаем имя для внешних ключей, используя вспомогательный метод Map(). В результате, Entity Framework добавит внешний ключ как в таблицу Customers, так и в таблицу Profiles:

Два внешних ключа в каждой из таблиц при отношении ноль-или-один-к-ноль-или-одному

Фактически, отношение ноль-или-один-к-ноль-или-одному идентично двум отношениям ноль-или-один-к-одному между нашими таблицами.

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