Entity framework типы данных

Обновлено: 30.06.2024

Под катом много кода.

Немного теории

Ядром архитектуры Domain Driven Design является Домен — предметная область, к которой применяется разрабатываемое программное обеспечение. Здесь находится вся бизнес-логика приложения, которая обычно взаимодействует с различными данными. Данные могут быть двух типов:

  • Entity Object
  • Value Object (далее — VO)

Entity может содержать другие Entity и VO. В состав VO могут быть включены другие VO, но не Entity.

Таким образом, логика домена должна работать исключительно с Entity и VO — этим гарантируется его консистентность. Базовые типы данных, такие как string, int и т.д. зачастую не могут выступать в качестве VO, потому что могут элементарно нарушить состояние домена — что в рамках DDD является почти катастрофой.

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


Просто и наглядно — идентификатор, имя и возраст, где же тут можно ошибиться?

А ошибок тут может быть несколько — например, с точки зрения бизнес-логики, имя обязательно, не может быть нулевой длины или более 100 символов и не должно содержать спецсимволы, пунктуацию и т.д. А возраст не может быть меньше 10 или больше 120 лет.

С точки зрения языка программирования, 5 — вполне нормальное целое число, аналогично и пустая строка. А вот домен уже находится в некорректном состоянии.

Переходим к практике

К этому моменту мы знаем, что VO должен быть иммутабельным и содержать значение, допустимое для бизнес-логики.

Иммутабельность достигается инициализацией readonly свойства при создании объекта.
Проверка допустимости значения происходит в конструкторе (Guard clause). Саму проверку желательно сделать доступной публично — для того, чтобы, другие слои могли провалидировать данные поступившие от клиента (тот же браузер).

Давайте создадим VO для Name и Age. Дополнительно немного усложним задачу — добавим PersonalName, объединяющий в себе FirstName и LastName, и применим это к Person.

И, наконец, Person:


Таким образом, мы не можем создать Person без полного имени или возраста. Также мы не можем создать “неправильное” имя или “неправильный” возраст. А хороший программист обязательно проверит в контроллере поступившие данные с помощью методов Name.IsValid(“John”) и Age.IsValid(35) и в случае некорректных данных — сообщит об этом клиенту.

Если мы возьмем за правило везде в модели использовать только Entity и VO, то убережем себя от большого количества ошибок — неправильные данные просто не попадут в модель.

Persistence

Теперь нам нужно сохранить наши данные в хранилище данных и получить их по запросу. В качестве ORM будем использовать Entity Framework Core, хранилище данных — MS SQL Server.

DDD четко определяет: Persistence — это подвид инфраструктурного слоя, поскольку скрывает в себе конкретную реализацию доступа к данным.

Домен ничего не должен знать про Persistence, по этому определяет только интерфейсы репозиториев.

А Persistence содержит в себе конкретные реализации, конфигурации маппинга, а также объект UnitOfWork.

Существует два мнения, стоит ли создавать репозитории и Unit of Work.

С одной стороны — нет, не нужно, ведь в Entity Framework Core это все уже реализовано. Если у нас многоуровневая архитектура вида DAL -> Business Logic -> Presentation, которая отталкивается от хранения данных — то почему бы и не использовать возможности EF Core напрямую.

Но домен в DDD не зависит от хранения данных и используемого ORM — это всё тонкости имплементации, которые инкапсулированы в Persistence и никого больше не интересуют. Если мы предоставляем DbContext в другие слои, то тут же раскрываем детали имплементации, намертво завязываемся на выбранную ORM и получаем DAL — как основу всей бизнес-логики, а такого быть не должно. Грубо говоря, домен не должен заметить изменение ORM и даже потерю Persistence как слоя.

Итак, интерфейс репозитория Persons, в домене:


и его реализация в Persistence:


Казалось бы, ничего сложного, но есть проблема. Entity Framework Core “из коробки” работает только с базовыми типами (string, int, DateTime и т.д.) и ничего не знает про PersonalName и Age. Давайте научим EF Core понимать наши Value Objects.

Configuration

Для конфигурирования Entity в DDD больше всего подходит Fluent API. Атрибуты не подходят, так как домен не должен ничего знать про нюансы маппинга.

Создадим в Persistence класс с базовой конфигурацией PersonConfiguration:


и подключим его в DbContext:

Mapping

Тот раздел, ради которого и написан этот материал.

В данный момент есть два более-менее удобных способа маппинга нестандартных классов к базовым типам — Value Conversions и Owned Types.

Value Conversions

Эта фича появилась в Entity Framework Core 2.1 и позволяет определять конвертацию между двумя типами данных.

Напишем конвертер для Age (в этом разделе весь код — в PersonConfiguration):


Простой и лаконичный синтаксис, но не обошлось без недостатков:

  1. Невозможно конвертировать null;
  2. Невозможно конвертировать одно свойство в несколько колонок в таблице и наоборот;
  3. EF Core не умеет преобразовывать LINQ выражение с этим свойством в SQL запрос.


Здесь есть условие по возрасту, но EF Core не сможет его преобразовать в SQL запрос и, дойдя до Where(), загрузит всю таблицу в память приложения и, только потом, с помощью LINQ, выполнит условие p.Age.Value > age.Value.

В общем, Value Conversions — простой и быстрый вариант маппинга, но нужно помнить о такой особенности работы EF Core, иначе, в какой то момент, при запросе в большие таблицы, память может закончиться.

Owned Types

Owned Types появились в Entity Framework Core 2.0 и пришли на замену Complex Types из обычного Entity Framework.

Давайте сделаем Age как Owned Type:


Неплохо. А еще Owned Types не имеют некоторых недостатков Value Conversions, а именно пунктов 2 и 3.

2. Возможно конвертировать одно свойство в несколько колонок в таблице и наоборот

То, что нужно для PersonalName, хотя синтаксис уже немного перегружен:


3. EF Core умеет преобразовывать LINQ выражение с этим свойством в SQL запрос.
Добавим сортировку по LastName и FirstName при загрузке списка:


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

Конечно, есть и недостатки.

  1. Никуда не делись проблемы с null;
  2. Поля Owned Types не могут быть readonly и должны иметь protected или private сеттер.
  3. Owned Types реализованы как регулярные Entity, что означает:
    • У них есть идентификатор (как shadow property, т.е. он не фигурирует в доменном классе);
    • EF Core трекает все изменения в Owned Types, точно так же, как и для обычных Entity.

С другой стороны — это такие детали реализации, которые можно опустить, но, опять же, забывать не стоит. Трекинг изменений влияет на производительность. Если с выборками единичных Entity (например, по Id) или небольших списков это не заметно, то с выборкой больших списков “тяжелых” Entity (много VO-свойств) — просадка в производительности будет весьма заметной именно из-за трекинга.

Presentation

Мы разобрались как реализовать Value Objects в домене и репозитории. Пришло время все это использовать. Создадим две простейшие странички — со списком Person и формой добавления Person.

Код контроллера без Action методов выглядит так:


Добавим Action для получения списка Person:

Ничего сложного — загрузили список, создали Data-Transfer Object (PersonModel) на каждый

Person и отправили в соответствующую View.



Гораздо интереснее добавление Person:

Здесь присутствует обязательная валидация входящих данных:

Здесь нужно сделать небольшое отступление — в Asp Net Core есть штатный способ валидации данных — с помощью атрибутов. Но в DDD такой способ валидации не является корректным по нескольким причинам:

  • Возможностей атрибутов может не хватать для логики валидации;
  • Любую бизнес-логику, в том числе и правила валидации параметров, устанавливает исключительно домен. У него монопольное право на это и все остальные слои должны с этим считаться. Атрибуты можно использовать, но полагаться на них не стоит. Если атрибут пропустит некорректные данные, то мы опять получим исключение при создании VO.

Заключение

Я привел примеры реализации Value Objects в общем и нюансы маппинга в Entity Framework Core. Надеюсь, что материал пригодится в понимании того, как применять элементы Domain Driven Design на практике.

Полный исходный код проекта PersonsDemo — GitHub

В материале не раскрыта проблема взаимодействия с опциональными (nullable) Value Objects — если бы PersonalName или Age были не обязательными свойствами Person. Я хотел это описать в данной статье, но она и так вышла несколько перегруженной. Если есть интерес к этой проблематике — пишите в комментариях, продолжение будет.

Фанатам “красивых архитектур” в общем и Domain Driven Design в частности очень рекомендую ресурс Enterprise Craftsmanship.

Также использовалась официальная документация по Owned Types и Value Conversions.

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

Включение типов в модель

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

В приведенном ниже примере кода включены все типы:

  • Blog включен, так как он предоставляется в свойстве DbSet в контексте.
  • Post включен, так как он обнаруживается через Blog.Posts свойство навигации.
  • AuditEntry так как он указан в OnModelCreating .

Исключение типов из модели

Если вы не хотите включать тип в модель, его можно исключить:

Исключение из миграции

Возможность исключения таблиц из миграции была введена в EF Core 5,0.

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

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

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

Имя таблицы

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

Вы можете вручную настроить имя таблицы:

Схема таблицы

При использовании реляционной базы данных таблицы — это соглашение, созданное в схеме базы данных по умолчанию. например, Microsoft SQL Server будет использовать dbo схему (SQLite не поддерживает схемы).

Можно настроить создание таблиц в определенной схеме следующим образом.

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

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

Просмотр сопоставления

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

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

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

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

Сопоставление функций, возвращающих табличное значение

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

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

Теперь сущность BlogWithMultiplePosts может быть сопоставлена с этой функцией следующим образом:

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

Согласно соглашению, свойства сущности будут сопоставляться с соответствующими столбцами, возвращаемыми функцией. Если имена столбцов, возвращаемых ТАБЛИЧной функцией, отличаются от имен свойств сущностей, то столбцы сущности могут быть настроены с помощью HasColumnName метода, как и при сопоставлении с обычной таблицей.

Если тип сущности сопоставляется с функцией, возвращающей табличное значение, запрос:

Преобразуется в следующий запрос SQL:

Комментарии к таблице

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

Установка комментариев с помощью заметок к данным была представлена в EF Core 5,0.

Типы сущностей общего типа

Поддержка типов сущностей с общим типом была представлена в EF Core 5,0.

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

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

POCO Entities

POCO означает «старые» объекты CLR, которые можно использовать в качестве существующих объектов домена в вашей модели данных.

Классы данных POCO, которые отображаются на объекты, определяются в модели данных.

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

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

POCO означает «старые» объекты CLR, которые можно использовать в качестве существующих объектов домена в вашей модели данных.

Классы данных POCO, которые отображаются на объекты, определяются в модели данных.

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

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

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

Концептуальная модель сущности

Дизайнерское окно

Генерация кода

В обозревателе решений вы увидите, что генерируются шаблоны POCODemo.Context.tt и POCODemo.tt.

POCODemo.Context генерирует DbContext и наборы объектов, которые вы можете возвращать и использовать для запросов, скажем, для контекста, студентов и курсов и т. Д.

генерировать

Другой шаблон имеет дело со всеми типами Student, Courses и т. Д. Ниже приведен код для класса Student, который автоматически генерируется из Entity Model.

Аналогичные классы создаются для таблиц курса и регистрации из модели сущностей.

Динамический прокси

При создании экземпляров типов сущностей POCO, Entity Framework часто создает экземпляры динамически генерируемого производного типа, который действует как прокси для сущности. Также можно сказать, что это прокси-классы времени выполнения, такие как класс-оболочка объекта POCO.

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

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

Этот метод также применим к тем моделям, которые созданы с использованием Code First и EF Designer.

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

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

Этот метод также применим к тем моделям, которые созданы с использованием Code First и EF Designer.

Если вы хотите, чтобы Entity Framework поддерживал отложенную загрузку связанных объектов и отслеживал изменения в классах POCO, то классы POCO должны соответствовать следующим требованиям:

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

Пользовательский класс данных не должен быть запечатан.

Пользовательский класс данных не должен быть абстрактным.

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

Используйте защищенный конструктор без параметров, если вы хотите, чтобы метод CreateObject использовался для создания прокси для объекта POCO.

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

Класс не может реализовать интерфейсы IEntityWithChangeTracker или IEntityWithRelationships, потому что прокси-классы реализуют эти интерфейсы.

Параметр ProxyCreationEnabled должен иметь значение true.

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

Пользовательский класс данных не должен быть запечатан.

Пользовательский класс данных не должен быть абстрактным.

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

Используйте защищенный конструктор без параметров, если вы хотите, чтобы метод CreateObject использовался для создания прокси для объекта POCO.

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

Класс не может реализовать интерфейсы IEntityWithChangeTracker или IEntityWithRelationships, потому что прокси-классы реализуют эти интерфейсы.

Параметр ProxyCreationEnabled должен иметь значение true.

В следующем примере показан класс динамического прокси-объекта.

Чтобы отключить создание прокси-объектов, установите для свойства ProxyCreationEnabled значение false.

Только учась может успокоить сердце.

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

1. Отображение таблицы [Таблица (имя строки, Свойства: [Схема = строка])


2. Отображение столбца [Column (имя строки, свойства: [Order = int], [TypeName = string])


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

3. Первичный ключ [ключ]

Диаграмма базы данных: опущена

4. Составной первичный ключ


5. Вычисляемый столбец (вычисление или объединение столбцов): не может быть реализован в аннотациях данных и может быть реализован только в Fluent API.

Диаграмма базы данных: опущена

6. Последовательность: не может быть реализована в аннотации данных, только в Fluent API

Вы можете настроить его на запуск с фактического значения 1000: StartsAt (1000), приращение 5: IncrementsBy (5)

Вы также можете взять значения из модели, например, следующее СЛЕДУЮЩЕЕ ЗНАЧЕНИЕ ДЛЯ shared.OrderNumbers

7. Значение по умолчанию: не может быть реализовано в аннотации данных (в отличие от EF, даже если оно предоставлено, но не оказывает влияния), может быть реализовано только в Fluent API

Добавление [DefaultValue (3)] в поле не имеет никакого эффекта

8. Индекс: он не может быть реализован в аннотации данных (в отличие от EF, даже если он предоставлен, но не имеет никакого эффекта), он может быть реализован только в Fluent API

Это уникально: IsUnique ()


9: ограничения внешнего ключа



Много ко многим:

10: Исключить сущности и атрибуты - NotMapped

11: максимальная длина-MaxLength (в Fluent API нет MinLength)

12: Анти-параллелизм: отметка времени и проверка параллельности

Пример с 14 Во время миграции базы данных EF вставит некоторые данные в базу данных

Результат после переноса данных:


В дополнение к базовым строкам преобразования перечислений EF Core также предоставляет следующие классы преобразования:

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

В дополнение к этому методу EF Core также поддерживает непосредственное указание типов, таких как:

Следует отметить, что значение null нельзя преобразовать, а атрибут можно преобразовать только для одного столбца.

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

резюме

1: В конце написания статьи я все еще не придерживался всего этого. Например, 15, 16, 17 не приводили примеры. На самом деле, я чувствую, что они довольно важны. У меня есть время, чтобы наверстать упущенное.

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