Что такое навигационное свойство entity framework
Для связей между моделями в Entity Framework Core применяются внешние ключи и навигационные свойства. Так, возьмем к примеру следующие модели:
В данном случае сущность Company является главной сущностью, а класс User - зависимой, так как содержит ссылку на класс Company и зависит от этого класса.
Свойство CompanyId в классе User является внешним ключом , а свойство Company - навигационным свойством. По умолчанию название внешнего ключа должно принимать одно из следующих вариантов имени:
Имя_навигационного_свойства+Имя ключа из связанной сущности - в нашем случае имя навигационного свойства Company, а ключа из модели Company - Id, поэтому в нашем случае нам надо обозвать свойство CompanyId , что собственно и было сделано в вышеприведенном коде.
Имя_класса_связанной_сущности+Имя ключа из связанной сущности - в нашем случае класс Company, а имя ключа из модели Company - Id, поэтому опять же в этом случае получается CompanyId
Свойство Users , представляющее список пользователей компании, в классе Company также является навигационным свойством.
В итоге после генерации базы данных таблица Users будет иметь следующее определение:
Но нам необязательно определять внешний ключ в зависимой сущности. Его можно опустить:
В этом случае Entity Framework сам автоматически сгенерирует столбец для внешнего ключа в таблице Users. Преимущество определения внешнего ключа в качестве свойства состоит в том, что в каких-то ситуациях нам может потребоваться только id связанной сущности. Тем более столбец для внешнего ключа в таблице в любом случае создается.
Более того, мы можем вовсе опустить навигационное свойство в классе User:
Но за счет того, что в классе Company также определено навигационное свойство Users все равно будет создаваться внешний ключ и связь таблицы Users и таблицы Companies. В частности, в этом случае определение таблицы Users будет выглядеть следующим образом:
В отличие от первой версии таблицы здесь не добавляется каскадное удаление.
Настройка ключа с помощью аннотаций данных
В принципе название свойства - внешнего ключа необязательно должно следовать выше описанным условностям. Чтобы установить свойство в качестве внешнего ключа, применяется атрибут [ForeignKey] :
В этом случае на уровне базы данных для этой модели будет генерироваться следующая таблица:
Настройка ключа с помощью Fluent API
Для настройки отношений между моделями с помощью Fluent API применяются специальные методы: HasOne / HasMany / WithOne / WithMany . Методы HasOne и HasMany устанавливают навигационное свойство для сущности, для которой производится конфигурация. Далее могут идти вызовы методов WithOne и WithMany , который идентифицируют навигационное свойство на стороне связанной сущности. Методы HasOne/WithOne применяются для обычного навигационного свойства, представляющего одиночный объект, а методы HasMany/WithMany используются для навигационных свойств, представляющих коллекции. Сам же внешний ключ устанавливается с помощью метода HasForeignKey :
Кроме того, с помощью Fluent API мы можем связь внешнего ключа не только с первичными ключами связанных сущностей, но и с другими свойствами. Например:
Метод HasPrincipalKey указывает на свойство связанной сущности, на которую будет ссылаться свойство-внешний ключ CompanyName. Кроме того, для свойства, указанного в HasPrincipalKey() , будет создавать альтернативный ключ.
Причем при использовании классов нам достаточно установить либо одно навигационное свойство, либо свойство-внешний ключ. Например, укажем значение только для навигационного свойства:
В этой статье приводятся общие сведения о том, как 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.
При работе с сущностями, участвующими в проверке и разрешении параллелизма, рекомендуется всегда использовать связь по внешнему ключу.
Дополнительные сведения см. в разделе Обработка конфликтов параллелизма.
Работа с перекрывающимися ключами
Перекрывающиеся ключи представляют собой составные ключи, некоторые из свойств в которых также являются частью другого ключа в сущности. Для независимых сопоставлений использовать перекрывающиеся ключи нельзя. Для изменения сопоставления на основе внешнего ключа, содержащей перекрывающиеся ключи, рекомендуется изменять значения внешнего ключа вместо использования ссылок на объекты.
Хочу рассказать о Lazy Loading в Entity Framework и почему использовать его надо с осторожностью. Сразу скажу, что я предполагаю что читатель использует Entity Framework или хотя бы читал про него.
Что такое Lazy Loading?
Lazy loading это способность EF автоматически подгружать из базы связанные сущности при первом обращении к ним. Например, рассмотрим класс Trade:
При первом обращении к свойствам Buyer или Seller они будут автоматически загружены из базы. Технически это реализовано через создание экземпляров прокси классов-наследников класса Trade. У прокси класса обращение к свойствам переопределено и содержит логику по загрузке данных из базы. Соответственно что бы механизм работал свойства должны быть виртуальными.
Что это дает?
В теории lazy loading дает возможность подгружать только те данные которые реально используются и нужны в работе. Облегчает получение связанных сущностей и потенциально должен ускорять работу.
А что же в этом плохого?
Как обычно за все надо платить, вот некоторые минусы которые обязательно следует учесть при использование lazy loading.
Нарушает согласованность данных.
Рассмотрим следующий пример:
Представим что в промежутке между загрузкой заказа из базы и обращением к свойству OrderLines содержимое заказа было изменено. В результате мы получим содержимое заказа на текущий момент, а не на момент загрузки самого заказа из базы.
Может ухудшать производительность.
Как же так, спросите вы, он же предназначен для ускорения?? В теории да, а вот на практике мне не раз приходилось решать проблемы, появившиеся в результате неграмотного использования данной фичи. Простой пример:
Вопрос, сколько запросов будет отправлено в базу? Правильно, 3. В данном случае, возможно, это и не очень то страшно.
Но давайте представим что теперь мы хотим получать информацию сразу по произвольному количеству сделок. Модифицируем наш метод:
В результате количество запросов возрастет пропорционально количеству запрашиваемых сделок. Представьте что при запросе информации по сотне сделок в базу полетит >100 запросов, хотя хватило бы и одного. При этом подобные ошибки не так то просто отловить на этапе разработки/ревью и подобный код может выстрелить уже в боевой среде.
Аналогичная проблема возникает при попытке сериализовать объекты с поддержкой lazy loading, сериализатор будет дергать объект за каждое свойство генерируя таким образом дополнительные обращения к базе данных.
Текучая абстракция
При использовании lazy loading наши POCO объекты перестают быть POCO, ведь теперь мы работаем с прокси которые хранят ссылку на контекст и сами ходят в базу. Что будет если пользоваться этими объектами вне контекста который их загрузил?
Заключение
Лично я в своих проектах принял решение не использовать lazy loading совсем, возможно это слишком категорично, но основываясь на своем опыте скажу, что проблем от него я получил куда больше чем пользы.
Есть альтернативный вариант, оставить lazy loading включенным но строго следить за тем что бы все навигационные свойства не были виртуальными (за тем редким исключением где руки чешутся очень необходим lazy loading).
Приветствую читателя! В данной статье я расскажу об основах работы с Entity Framework Core (далее EF Core), а именно:
- подключение EF Core к проекту;
- связывание классов-моделей и таблиц базы данных;
- связи между моделями (один к одному, один ко многим, многие ко многим);
- подходы Code First и Database First;
- создание миграций;
- выполнение CRUD операций,
- виды загрузки связанных данных (Eager/Explicit/Lazy).
В качестве СУБД используется PostgreSQL.
Об Entity Framework Core
Entity Framework Core — это объектно-ориентированная ORM система для упрощенного доступа к данным из реляционных СУБД.
ORM — Object-Relational Mapping — технология, которая связывает базу данных с концепциями объектно-ориентированных языков программирования.
Ещё одно удобство заключается в том, что при необходимости сменить СУБД в проекте с EF Core потребуется всего лишь сменить провайдера и незначительно изменить код конфигурации и подключения. Весь остальной код, который реализует получение информации и осуществление выборки данных, остается неизменным. Поэтому можно утверждать, что EF Core является абстракцией над СУБД.
Актуальная версия EF Core на момент публикации статьи — EF Core 5.0.
Подключение EF Core и подготовка проекта
Пакет Npgsql.EntityFrameworkCore.PostgreSQL является провайдером для СУБД PostgreSQL.
После успешной установки пакетов создадим класс контекста ApplicationContext, который будет являться точкой доступа к базе данных.
Класс ApplicationContext наследуется от класса DbContext, который представляет сессию подключения к базе данных. При создании экземпляра будет производиться подключение к БД при помощи строки подключения, которая должна передаваться в функцию UseNpgsql в качестве параметра.
В данном примере присутствует большой недостаток: мы захардкодили строку подключения. Это плохо с точки зрения проектирования: если мы захотим сменить строку подключения, то придется перекомпилировать весь проект. Решение проблемы — вынос строки подключения в файл конфигурации.
Добавим в проект файл конфигурации App.config со следующим содержанием:
Изменим в классе ApplicationContext способ передачи строки подключения в метод UseNpgsql:
Мы успешно подключили фреймворк к проекту. Двигаемся дальше.
Благо EF Core обладает встроенными средствами для генерации базы данных по моделям (подход Code First) и генерации моделей по существующей базе данных (подход Database First). При обоих подходах таблицы и модели связываются автоматически.
Подход Code First + связь один ко многим
Рассмотрим первый подход — Code First: по существующим классам-моделям EF Core автоматически генерирует базу данных со всеми таблицами и связывает их с классами.
Создадим 2 класса Teacher и Student, согласно следующей диаграмме:
Учитель по отношению к студенту имеет связь один ко многим (студент к учителю — многие к одному). Т.е. у студента может быть только один учитель, а у учителя множество студентов.
Свойства Teacher в классе Student и List<Student> в классе Teacher называются навигационными свойствами.
По какой причине навигационные свойства следует делать виртуальными будет рассказано позже.
В классах моделей могут содержаться различные методы, это не повлияет на функциональность в рамках фреймворка. В данном случае переопределены методы ToString.
У классов имеются одинаковые поля: Id, Name, Surname, Age. Эти поля можно было вынести в базовый класс Human и наследоваться от него нашими моделями Teacher и Student. Это также не помешало бы работе фреймворка.
Добавим в класс ApplicationContext свойства параметризированного типа DbSet<T>, через которые мы будем взаимодействовать с записями переданных типов из базы данных.
Для генерации базы данных с таблицами мы должны создать миграцию. Это делается при помощи команды Add-Migration [Name], где Name — имя миграции. Команда вводится в консоль диспетчера пакетов.
После выполнения команды в проекте сгенерируются папки с миграциями и несколько классов, отвечающих за генерацию базы данных. В данном случае нам не требуется вникать в функционал этих классов.
Для применения миграции следует обновить базу данных командой Update-Database.
После выполнения команды создается база данных со структурой соответствующей диаграмме классов. Также добавляется одна служебная таблица с информацией о миграциях.
Сущность учителя в созданной базе данных ничего «не знает» о своих учениках в отличии от класса-модели, который имеет навигационное свойство для получения информации об учениках.
Таблица Student имеет внешний ключ, который ссылается на id учителя. Как и следовало ожидать.
Поздравляю! Мы успешно настроили взаимосвязь компонентов при помощи подхода Code First. Пришло время изучить методы для взаимодействия с данными.
CRUD операции
Основные взаимодействия с данными — это CRUD операции:
- С — Create — создание/добавление данных;
- R — Read — получение/чтение данных;
- U — Update — обновление/изменение данных;
- D — Delete — удаление данных.
Давайте добавим информацию в базу данных (операция Create):
Код создает экземпляры двух учителей и трёх студентов, добавляет этих студентов в коллекции студентов внутри экземпляров учителя и добавляет учителей в базу данных.
При создании экземпляров моделей мы не указали в них Id, они остались равные нулю. При добавлении информации в базу данных идентификаторы генерируются автоматически.
Класс DbContext, от которого мы унаследовали наш класс ApplicationContext, реализует интерфейс IDisposable, поэтому требуется вызов метода Dispose, когда экземпляр больше не нужен. Если создать экземпляр в конструкции using, то Dispose вызывается автоматически.
При добавлении экземпляров в базу данных, также добавляются все зависимые сущности из навигационных свойств, поэтому ученики из коллекций учителей тоже занесены в базу данных.
Попробуем получить добавленные данные (операция Read):
Изменим у полученного учителя данные, сохраним результат (операция Update) и снова получим данные:
Удалим учителя с именем Anna (операция Delete):
Вместе с учителем удаляются студенты, которые были закреплены за ним, т.к. включено каскадное удаление.
Как видно на рисунке выше, мы можем фильтровать получаемые данные при помощи функций Linq и лямбда выражений. Эти функции применяются к коллекциям DbSet. В данном случае была использована функция Where для фильтрации результата по одному из свойств.
Итак, мы разобрали основные операции с данными. Не сложно, не так ли? И всё это без непосредственной работы с языком запросов SQL. Далее рассмотрим взаимодействие со связанными данными.
Загрузка связанных данных
В предыдущем примере мы удалили учителя с одним учеником. У оставшегося в базе данных учителя 2 ученика. Давайте проверим это.
Почему же получилось, что у учителя нет студентов? Дело в том, что зависимые сущности не запрашиваются у базы данных автоматически, мы должны самостоятельно указать «что» и «когда» загрузить.
В EF Core существуют три вида загрузки связанных данных:
- Eager loading (жадная загрузка);
- Explicit loading (явная загрузка);
- Lazy loading (ленивая загрузка).
Рассмотрим подробнее каждый вид.
Первый способ — Eager loading. Она выполняется при помощи вызова метода Include у объекта DbSet с указанием в лямбда-выражении требуемой сущности.
Особенность жадной загрузки в том, что зависимые данные загружаются в навигационные свойства момент первого обращения (в рамках одной сессии) к родительскому объекту.
Далее — Explicit loading. Явная загрузка осуществляется при помощи метода Load. Причем для получения только тех студентов, которые закреплены за нужным нам учителем, можно отфильтровать их по внешнему ключу учителя.
Данный способ примечателен тем, что мы сами выбираем когда загрузить связанные данные и стоит ли их загружать вообще.
Существует второй способ явной загрузки: при помощи метода Entry в комбинации с методами Collection и Reference. Collection применяется, когда навигационное свойство является коллекцией объектов. Reference используется для загрузки одного объекта. В нашем случае (при получении студентов через учителя) студентов следует получать методом Collection.
Если нам потребуется получить учителя через студента, то используем Reference.
И наконец, Lazy loading. Для ленивой загрузки используется установленный ранее пакет Microsoft.EntityFrameworkCore.Proxies.
Данный тип загрузки налагает 2 ограничения на классы моделей:
- они должны быть открыт для наследования;
- навигационные свойства должны быть объявлены как виртуальные.
Прежде всего нужно добавить вызов функции UseLazyLoadingProxies в конфигурацию контекста данных ApplicationContext:
Ленивая загрузка производится в момент первого обращения к навигационному свойству. В данном случае это происходит, когда мы пытаемся получить количество элементов в коллекции студентов. Никаких дополнительных функций вызывать не нужно, мы абстрагируемся от этого процесса.
Подход Database First + связь многие ко многим
Мы рассмотрели подход Code First, теперь рассмотрим — Database First: по существующей базе данных EF Core автоматически генерирует классы моделей и связывает их с таблицами.
Создадим 2 сущности в базе данных: автор и книга. Один автор может написать несколько книг, одна книга может быть написана несколькими авторами. При таких условиях реализуется связь многие ко многим.
Сущность автора содержит информацию об имени, фамилии и возрасте. В качестве первичного ключа выступает числовой идентификатор.
Сущность книги: название и год. Первичный ключ — также числовой идентификатор.
Для реализации связи по типу многие ко многим нужна промежуточная таблица, которая будет состоять из двух внешних ключей, которые ссылаются на идентификаторы книг и авторов. Оба столбца будут являться составным первичным ключом.
Связь многие ко многим реализуется за счёт две связи один ко многим в промежуточной таблице.
После этого мы можем сгенерировать классы-модели при помощи следующей команды:
Scaffold-DbContext «Connection String» -Tables [tables]
, где Connection String — строка подключения к базе данных в следующем формате:
Host=XX;Port=XX;Database=XX;Username=XX;Password=XX;
[tables] — таблицы, на основе которых будут генерироваться модели.
Читайте также: