Entity framework сопоставление типов
Настройка типов столбцов
Ранее, при рассмотрении примера использования Code-First, вы уже видели использование некоторых атрибутов метаданных, которые позволяли настраивать тип данных столбца в таблице базы данных и применять к нему ограничения (например, указав поддерживает ли он NULL-значения). Далее мы рассмотрим эти атрибуты более подробно.
Ограничение длины
В таблице ниже показаны соглашения об ограничении длины столбцов, их реализация в виде аннотаций и в Fluent API:
В настройках модели можно указывать ограничение на максимальную длину для свойств, имеющих тип String или byte[], которые отображаются в таблице на соответствующие типы NVARCHAR и VARBINARY. По умолчанию Code-First задает для них максимальную длину NVARCHAR(max) и VARBINARY(max).
Ограничение длины можно наложить на строки или массивы байт. Согласно соглашениям, Code-First использует только ограничение максимальной длины, это означает, что SQL Server устанавливает тип данных для строк и массивов байт как NVARCHAR(n) и VARBINARY(n), где n – это длина, указанная в ограничении. По умолчанию, если к свойствам модели не использовалось ограничение по длине, Code-First установит максимально возможную длину столбцов – NVARCHAR(max) и VARBINARY(max).
Стоит обратить внимание, что указать максимальную и минимальную длину поля можно в одном атрибуте StringLength, используя именованные параметры этого атрибута. В следующем примере показано использование ограничения по длине с помощью аннотаций (здесь мы используем пример модели, который создали в статье “Использование Code-First” ранее):
А в следующем коде аналогичная настройка производится с помощью Fluent API (напомню, что для использования этого API-интерфейса необходимо переопределить метод настройки конфигурации OnModelCreating() в классе контекста, которым в нашем примере является класс SampleContext):
Явное указание типа столбца
Тип данных столбца в базе по умолчанию определяется поставщиком базы данных, которую вы используете. В SQL Server тип String в свойстве модели отражается на тип NVARCHAR в таблице базы данных, тип byte[] отражается на тип VARBINARY и т.д. В настройках модели можно явно указывать тип данных, который будет использоваться в таблице.
Как описывалось ранее, Entity Framework автоматически отображает типы данных модели на SQL-совместимые типы данных. Code-First позволяет управлять этим процессом, для того чтобы явно указать тип данных для столбца, как показано в примере ниже:
Поддержка NULL-значений для столбца
Entity Framework автоматически указывает для типов string и byte[] поддержку NULL в таблице базы данных, а для типов значений (int, long, …), DateTime, char, bool поддержку NOT NULL.
В примере ниже показано использование этих настроек, для конфигурирования информации об обнуляемых или не обнуляемых типах в таблице базы данных:
Обратите внимание, что в Fluent API можно настроить только поддержку NOT NULL для ссылочных типов данных, и нельзя настроить поддержку NULL для типов значений, т.к. поддержка NULL для них указывается явно, при объявлении типа свойства в классе модели.
Установка первичных ключей
Entity Framework ищет свойства модели имеющие имя Id или состоящие из строки “[TypeName] + Id”, например CustomerId для нашего примера и автоматически устанавливает их как первичные ключи в таблице базы данных.
Entity Framework требует, чтобы каждый класс сущностной модели имел уникальный ключ (т.к. для каждой таблицы в реляционной базе данных должен использоваться первичный ключ). Этот ключ используется в объекте контекста, для отслеживания изменений в объектах модели. Code-First делает некоторые предположения, при поиске ключа в таблице. Например, когда мы сгенерировали базу данных для классов сущностей Customer и Order ранее, при рассмотрении подхода Code-First, то Entity Framework пометил поля CustomerId и OrderId в таблицах, как первичные ключи и задал для них поддержку не обнуляемых типов:
Также EF автоматически добавил поддержку автоинкремента в эти поля (напомню, что в T-SQL это делается с помощью инструкции IDENTITY). Чаще всего, первичные ключи в базе данных имеют тип INT или GUID, хотя любой примитивный тип может быть использован в качестве первичного ключа. Первичный ключ в базе данных может состоять из нескольких столбцов таблицы, аналогично, ключ сущностной модели EF может быть составлен из нескольких свойств модели. Позже вы увидите как настроить составные ключи.
Явная установка первичных ключей
В случае двух наших классов Customer и Order беспокоиться об явном указании первичного ключа не стоит, т.к. мы используем свойства CustomerId и OrderId, которые соответствуют соглашению об именовании ключей - “[TypeName] + Id”. Давайте рассмотрим пример, в котором нужно явно указать первичный ключ. Добавьте в файл модели следующий простой класс:
Нашей целью является указать, что свойство Identifier в этом классе является первичным ключом таблицы. Как видите имя этого свойства не соответствует соглашению Entity Framework об именовании первичных ключей.
Добавьте в класс контекста SampleContext описание новой таблицы:
Обратите внимание, что при использовании Fluent API метод HasKey() указывается после вызова метода Entity<T>(), а не после вызова Entity<T>().Property(), как это делалось в примерах выше, т.к. первичный ключ устанавливается на уровне таблицы, а не на уровне свойств.
Настройка автоинкремента для первичных ключей
Если свойство первичного ключа в модели имеет тип int, для него автоматически устанавливается счетчик в базе данных с помощью инструкции IDENTITY (1,1).
Как видно из таблицы, следуя соглашениям, Entity Framework указывает автоинкремент для свойств имеющих тип int. В созданной ранее таблице Project для первичного ключа указывается тип Guid, вследствие чего, EF не использует счетчик для этого поля, при создании таблицы в базе данных. Это показано на рисунке:
Давайте добавим в наш проект новую веб-форму, которую назовем DatabaseGenerated.aspx. В обработчике Page_Load добавьте следующий код, в котором мы добавляем новые данные в таблицу Project. В данном случае эти данные будут добавляться всякий раз, когда мы открываем страницу нашей веб-формы в браузере.
Запустите проект и откройте веб-форму DatabaseGenerated.aspx. В результате в таблицу Project добавится новая запись:
Ни базе данных, ни Entity Framework не известно, что мы хотели бы создавать новый идентификатор Guid для каждой новой записи, поэтому будет автоматически создан идентификатор, содержащий одни нули. Если вы обновите страницу в браузере (фактически в данном случае код попытается вставить новую запись в таблицу), то Entity Framework вернет исключение SqlException, которое возникает из-за того, что мы пытаемся вставить запись имеющую идентификатор, который уже существует в таблице, т.е. в данном случае срабатывает ограничение первичного ключа – он должен быть уникальным для каждой новой записи.
В результате, чтобы решить данную проблему, нам бы потребовалось генерировать уникальный идентификатор Guid в коде. Таблицы, использующие автоинкремент для первичных ключей лишены такой дополнительной работы, т.к. для каждой новой вставляемой записи, счетчик создает новое значение для первичного ключа, извлекая значение первичного ключа для последней записи и прибавляя к нему 1 (если использовалась конструкция IDENTITY(1,1)).
Чтобы решить эту проблему для ключей, имеющих тип отличный от int, нужно использовать атрибут метаданных DatabaseGenerated, в конструкторе которого указывается перечисление DatabaseGeneratedOption, имеющее три возможных значения:
None
База данных не создает никакого уникального значения для первичного ключа. Фактически, с помощью этой опции можно отключить автоматическое добавление автоинкремента к первичным ключам типа int.
Identity
При вставке значений в таблицу база данных создаст уникальное значение для первичного ключа.
Computed
Аналогично Identity, за тем лишь исключением, что первичный ключ будет генерироваться не только при вставке записей в таблицу, но и при их обновлении.
Измените класс модели таким образом, чтобы указать базе данных возможность создавать уникальный первичный ключ:
Запустите пример проекта и несколько раз обновите страницу, чтобы вставить несколько записей в таблицу и убедиться, что исключение больше не генерируется. На рисунке ниже показаны данные, добавленные в таблицу:
Обратите внимание на автоматически сгенерированные идентификаторы для проектов. Того же эффекта можно добиться, используя метод HasDatabaseGeneratedOption() в Fluent API:
Работа со сложными типами данных
В этом классе адрес проживания пользователя можно выделить в отдельный класс и сослаться на него:
По соглашениями Entity Framework разберет эту модель – как две отдельные таблицы. Но нашей целью является создание сложного типа из класса Address. Традиционный способ для создания сложного типа из класса Address представляет удаление идентификатора AddressId:
В дополнение к правилу, что сложный тип не должен иметь ключ, Code-First накладывает два других правила, которые должны быть выполнены для обнаружения сложного типа. Во-первых, сложный тип должен содержать только простые свойства. Во-вторых, в классе, который использует этот тип, не разрешается указывать тип коллекции для свойства сложного типа. Другими словами, если вы хотите использовать сложный тип Address в классе User, то свойство, имеющее этот тип, не должно быть помечено как List<Address> или использовать другую коллекцию.
Как показано на рисунке ниже, после запуска приложения Code-First распознает сложный тип и создает специальные поля в таблице User (не забудьте добавить объявление User в классе контекста):
Обратите внимание, как поля, описывающие адрес пользователя, названы: ИмяСложногоТипа_ИмяСвойства. Это является соглашением Entity Framework по именованию сложных типов.
Настройка сложных типов в обход соглашениям Code-First
Выше было сказано, что класс, описывающий сложный тип, должен иметь только простые свойства (т.е. не ссылающиеся на другие объекты). Это соглашение можно преодолеть, используя все те же средства. Ниже показан пример, в котором был добавлен новый сложный тип UserInfo, ссылающийся на другой тип FullName:
Благодаря указанию Code-First на то, что UserInfo является сложным типом с помощью атрибута ComplexType, мы преодолели ограничение, накладываемое на сложные типы при использовании соглашения по умолчанию.
Стоит отметить, что Code-First позволяет настраивать сложные типы, также, как и обычные таблицы с использованием Fluent API или аннотаций. Ниже показан пример, для настройки сложного типа Address:
На рисунке ниже показана структура таблицы User. Здесь вы можете увидеть, как EF именует свойства сложных типов с ссылками внутри и то, что EF накладывает ограничение на поле StreetAddress:
Описание других настроек
В этом разделе мы кратко рассмотрим все оставшиеся настройки столбцов таблицы, которые используются довольно редко в силу своих специфических особенностей.
Столбцы типа Timestamp
Тип данных TIMESTAMP в T-SQL указывает столбец, определяемый как VARBINARY(8) или BINARY(8), в зависимости от свойства столбца принимать значения NULL. Для каждой базы данных система содержит счетчик, значение которого увеличивается всякий раз, когда вставляется или обновляется любая строка, содержащая ячейку типа TIMESTAMP, и присваивает этой ячейке данное значение. Таким образом, с помощью ячеек типа TIMESTAMP можно определить относительное время последнего изменения соответствующих строк таблицы. (ROWVERSION является синонимом TIMESTAMP.)
Само по себе значение, сохраняемое в столбце типа TIMESTAMP, не представляет никакой важности. Этот столбец обычно используется для определения, изменилась ли определенная строка таблицы со времени последнего обращения к ней. Это позволяет решать вопросы параллельного доступа к таблице базы данных, позволяя блокировать другие потоки, если текущий поток изменил значения в строке.
В Code-First для указания на то, что столбец должен иметь тип TIMESTAMP должен использоваться одноименный атрибут Timestamp в аннотациях или метод IsRowVersion() в Fluent API, как показано в примере ниже:
Менее распространенным способом, для обеспечения безопасности при работе с параллельными потоками, является указание проверки параллелизма для каждого столбца. Такой способ может еще использоваться в СУБД, не поддерживающих типы Timestamp/Rowversion. При работе таким образом, поток не проверяет, изменилась ли запись в таблице, а просто блокирует доступ к ней для других потоков, пока сам не завершит процесс записи. Для указания столбцов, которые должны пройти проверку на параллелизм, используется атрибут ConcurrencyCheck в аннотациях, либо метод IsConcurrencyToken() в Fluent API.
Изменение кодировки строк с Unicode на ASCII
По умолчанию Entity Framework преобразует все строковые типы данных модели, такие как string или char, в строковые типы данных SQL, использующие двухбайтовую кодировку Unicode – NVARCHAR или NCHAR. Вы можете изменить это поведение, и явно указать EF использовать однобайтовую кодировку ASCII – соответственно будут использоваться типы VARCHAR и CHAR. Для этого нужно использовать метод IsUnicode() с переданным ему логическим параметром false в Fluent API. Аннотации не предоставляют возможность настройки кодировки строк.
Настоятельно не рекомендую вам использовать эту настройку, если планируется хранить строковую информацию в базе данных в символах, не совместимых с ASCII (например, русские символы).
Указание точности для типа Decimal
Для указания точности для типа Decimal (количество цифр в числе) и масштаба (количество цифр справа от десятичной точки в числе) можно использовать метод HasPrecision() с передачей ему двух параметров, который используется в Fluent API. Аннотации данных в Code-First не предлагают альтернативы этому методу. По умолчанию Entity Framework устанавливает точность 18, а масштаб 2 для типов Decimal.
В примере ниже показано использование этого метода, для свойства Cost таблицы Project, которую мы создали ранее, при рассмотрении первичных ключей:
Включение 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 вызова.
Сегодня мы немного поговорим про EntityFramework. Совсем чуть-чуть. Да, я знаю что к нему можно относиться по-разному, многие от него плюются, но за неимением лучшей альтернативы — продолжают использовать.
Что мы больше всего любим в прямом SQL? Скорость и простоту. Там, где "в лучших традициях ORM" надо выгрузить в память вагончик объектов и всем сделать context.Remove (ну или поманипулировать Attach-ем), можнo обойтись одним мааааленьким SQL-запросом.
Что мы больше всего не любим в прямом SQL? Правильно. Отсутствие типизации и взрывоопасность. Прямой SQL обычно делается через DbContext.Database.ExecuteSqlCommand , а оно на вход принимает только строку. Следовательно, Find Usages в студии никогда не покажет вам какие поля каких сущностей ваш прямой SQL затронул, ну и помимо прочего вам приходится полагаться на свою память в вопросе точных имён всех таблиц/колонок которые вы щупаете. А ещё молиться, что никакой лоботряс не покопается в вашей модели и не переименует всё в ходе рефакторинга или средствами EntityFramework, пока вы будете спать.
Так ликуйте же, адепты маленьких raw SQL-запросов! В этой статье я покажу вам как совместить их с EF, не потерять в майнтайнабильности и не наплодить детонаторов. Ныряйте же под кат скорее!
А чего конкретно хотим достичь?
Итак, в этой статье я покажу вам отличный подход, который раз и навсегда избавит вас от беспокойства о проблемах, которые обычно вызывает прямой SQL в тандеме с EntityFramework. Ваши запросы приобретут человеческий облик, будут находиться через Find Usages и станут устойчивы к рефакторингу (удалению/переименованию полей в сущностях), а ваши ноги потеплеют, язвы рассосутся, карма очистится .
Вот как будет выглядеть ваш прямой SQL после прочтения этой статьи:
Здесь мы видим, что при вызове .Stroke тип-параметрами мы указываем сущности (замапленные на таблицы), с которыми будем работать. Они же становятся аргументами в последующем лямбда-выражении. Если кратко, то Stroke пропускает переданную ему лямбду через парсер, превращая в таблицы, а в соответствующее имя колонки.
Как-то так. Теперь давайте просмакуем подробности.
Сопоставление классов и свойств с таблицами и колонками
Давайте освежим ваши знания Reflection-а: представьте что у вас есть класс (точнее Type ) и у вас есть строка с именем проперти из этого класса. Так же имеется наследник EF-ного DbContext -а. Имея оные две вилки и тапок вам надобно добыть имя таблицы, на которую мапится ваш класс и имя колонки в БД, на которую мапится ваша проперть. Сразу же оговорюсь: решение этой задачи будет отличаться в EF Core, однако же на основную идею статьи это никак не влияет. Так что я предлагаю читателю самостоятельно реализовать/нагуглить решение этой задачи.
Итак, EF 6. Требуемое можно достать через весьма популярную магию приведения EF-ного контекста к IObjectContextAdapter :
И, пожалуйста, не спрашивайте меня что же разработчики EntityFramework курили имели в виду, создавая такие лабиринты абстракций и что в нём означает каждый закоулочек. Честно признаюсь — я сам в этом лабиринте могу заблудиться и кусок выше я, не писал, а просто нашел и распотрошил.
Так, с таблицей вроде разобрались. Теперь имя колонки. Благо, оно лежит рядом, в маппингах контейнера сущности:
Так, и вот тут я сразу и крупными буквами предупреждаю читателя: копаться в EF-метаданных — это медленно! Кроме шуток. Поэтому кэшируйте вообще всё, до чего дотянетесь. В статье есть ссылка на мой код — там я уже озаботился кэшированием — можете пользоваться. Но все равно держите в голове: реальные концептуальные модели EF — стозёвные чудища, хранящие в себе взводы и дивизии различных объектов. Если вам нужно только соотношение тип-имя таблицы и тип/свойство — имя колонки, то лучше один раз достаньте и закэшируйте (только не напоритесь там на утечку памяти — не храните ничего от DbContext -а). В EF Core, говорят, с этим по-лучше.
Выражения
Самое скучное позади. Теперь — лямбда-выражения. Положим, мы хотим иметь метод Stroke , чтобы вызывать его можно было вот таким макаром:
Сам метод Stroke простой:
В его основе лежит метод Parse , который и делает всю основную работу. Как нетрудно догадаться, этот метод должен разбирать лямбда-выражение, полученное от интерполяции строки. Ни для кого не секрет, что шарповая интерполяция строк является синтаксическим сахаром для String.Format . Следовательно, когда вы пишете $"String containing and " , то компилятор преобразует эту конструкцию в вызов String.Format("String containing and ", varA, varB) . Первым параметром у этого метода идёт строка формата. В ней мы невооруженным глазом наблюдаем плейсхолдеры — , и так далее. Format просто заменяет эти плейсхолдеры на то, что идет после строки формата, в порядке, обозначенном цифрами в плейсхолдерах. Если плейсхолдеров больше, чем 4 — то интерполированная строка компилируется в вызов перегрузки String.Format от двух параметров: самой строки формата и массива, в который пакуются все, страждущие попасть в результирующую строку параметры.
Таким образом, что мы сейчас сделаем в методе Parse ? Мы клещами вытянем оригинальную строку формата, а аргументы форматирования пересчитаем, заменяя где надо на имена таблиц и колонок. После чего сами вызовем Format , чем и соберем оригинальную строку формата и обработанные аргументы в результирующую SQL-строку. Честное слово, это гораздо проще закодить чем объяснить :)
Дальше надо сделать небольшой финт ушами: как было сказано выше, если у интерполированной строки больше 4х плейсхолдеров, то она транслируется в вызов string.Format -а с двумя параметрами, второй из которых — массив (в форме new [] < . >). Давайте же обработаем эту ситуацию:
Теперь давайте пройдемся по образовавшейся коллекции arguments и, наконец, преобразуем каждый аргумент, который связан с параметрами нашей лямбды в имя таблицы/колонки, а всё, что не является отсылками к таблицам и колонкам — вычислим и закинем в список параметров запроса, оставив в параметрах формата , где i — индекс соответствующего параметра. Ничего нового для опытных пользователей ExecuteSqlCommand .
Чудно. Далее нам потребуется понять имеет ли очередной аргумент отношение к параметрам выражения. Ну то есть имеет форму p.Field1.Field2. , где p — параметр нашего выражения (то, что ставится перед лямбда-оператором => ). Потому как если не имеет — то надобно этот аргумент просто вычислить, а результат запомнить как параметр SQL-запроса, для последующего скармливания EF-у. Самый простой и топорный способ определить обращаемся ли мы к полю какого-либо из параметров — это следующие два метода:
В первом мы просто перебираем цепочку обращений к членам, пока не дойдем до корня (я назвал его GetRootMember ):
Во втором — собственно проверяем требуемые нам условия:
Готово. Возвращаемся к перебору аргументов:
Отлично. Мы отсекли все параметры, которые гарантированно не являются ссылками на наши таблицы/колонки. Список sqlParams потом вернётся через out -параметр — мы его наряду со строкой-результатом скормим context.Database.ExecuteSqlCommand вторым аргументом. Пока же обработаем ссылки на таблицы:
Тут нам придется отрезать возможность обращаться к агрегатам, ибо как это приведет к необходимости переколбашивать запрос JOIN -ами, чего мы технически сделать не можем. Так что — увы и ах. Если наш аргумент — это обращение к члену, но не к члену непосредственно параметра выражения — то звиняйте, ничем не можем помочь:
И вот, наконец, мы можем добыть наше имя колонки и добавить его в переработанный список формат-аргументов.
Теперь, когда все аргументы перебраны, мы можем наконец-таки сделать string.Format самостоятельно и получить SQL-строку и массив параметров, готовые к скармливанию ExecuteSqlCommand .
Готово
Вот как-то так. Для статьи я намеренно упростил код. В частности, полная версия автоматически подставляет алиасы таблиц, нормально кэширует имена таблиц и колонок, а так же содержит перегрузки .Stroke до 8 параметров. С полным исходным кодом вы можете ознакомитья в моем github. За сим прощаюсь и желаю всяческих удач в разработке.
Взаимосвязи EF Core - концепции и свойства навигации
В настоящее время у нас есть только один класс сущности (модели), класс Student , но довольно скоро мы собираемся создать остальную часть модели базы данных в нашем приложении. Но прежде чем мы это сделаем, очень важно понять некоторые основные концепции при работе с реляционными базами данных и моделями.
Когда мы создаем связь между двумя объектами, одна из них становится основной сущностью, а другая - зависимой сущностью. Основная сущность - это основная сущность во взаимоотношениях. Он содержит первичный ключ как свойство, на которое зависимая сущность ссылается через внешний ключ. Зависимая сущность, с другой стороны, - это сущность, которая содержит внешний ключ, который относится к первичному ключу основной сущности.
Наши классы сущностей будут содержать навигационные свойства, которые представляют собой свойства, содержащие один класс или набор классов, которые EF Core использует для связывания классов сущностей.
Кроме того, давайте объясним отношения Required и Optional в EF Core. Обязательная связь - это связь, в которой внешний ключ не может быть нулевым. Это означает, что должен существовать главный объект. Необязательное отношение - это отношение, в котором внешний ключ может иметь значение NULL и, следовательно, основной объект может отсутствовать.
Настройка One-to-One связи
Отношение "один к одному" означает, что строка в одной таблице может относиться только к одной строке в другой таблице в связи. Это не такая распространенная связь, потому что она обычно обрабатывается как «все данные в одной таблице», но иногда (когда мы хотим разделить наши сущности) полезно разделить данные на две таблицы.
Самый простой способ настроить этот тип отношений - использовать подход по соглашению, и это именно то, что мы собираемся сделать. Итак, давайте сначала создадим еще один класс в проекте Entities с именем StudentDetails :
Теперь, чтобы установить связь между классами Student и StudentDetails , нам нужно добавить свойство навигации по ссылкам с обеих сторон. Итак, давайте сначала изменим класс Student :
И давайте изменим класс StudentDetails :
Мы можем видеть, что класс Student имеет свойство навигации по ссылке к классу StudentDetails , а класс StudentDetails имеет внешний ключ и свойство навигации Student .
В результате мы можем создать новую миграцию и применить ее:
Отлично, отлично работает.
Дополнительные пояснения
Как мы объяснили в первой статье, EF Core ищет все общедоступные свойства DbSet<T> в классе DbContext для создания таблиц в базе данных. Затем он ищет все общедоступные свойства в классе T для сопоставления столбцов. Но он также выполняет поиск всех общедоступных свойств навигации в классе T и создает дополнительные таблицы и столбцы, связанные с типом свойства навигации. Итак, в нашем примере в классе Student EF Core находит свойство навигации StudentDetails и создает дополнительную таблицу со своими столбцами.
Конфигурация отношений One-to-Many
В этом разделе мы узнаем, как создавать отношения "один ко многим" всеми тремя способами. Итак, прежде чем мы начнем, давайте создадим дополнительный класс модели Evaluation в проекте Entities :
Использование условного подхода для создания отношений «один ко многим»
Давайте посмотрим на различные соглашения, которые автоматически настраивают связь "один ко многим" между классами Student и Evaluation .
Первый подход включает свойство навигации в основной сущности, классе Student :
В классе ApplicationContext есть свойство DbSet , и, как мы объяснили, EF Core выполняет поиск по классу Student , чтобы найти все свойства навигации для создания соответствующих таблиц в базе данных.
Еще один способ создать связь "один ко многим" - это добавить свойство Student в класс Evaluation без свойства ICollection в классе Student класс :
Чтобы этот подход работал, мы должны добавить свойство DbSet<Evaluation> Evaluations в класс ApplicationContext .
Третий подход по Конвенции заключается в использовании комбинации предыдущих. Итак, мы можем добавить свойство навигации ICollection<Evaluation> Evaluations в класс Student и добавить свойство навигации Student Student в класс Evaluation . Конечно, при таком подходе нам не нужно свойство DbSet<Evaluation> Evaluations в классе ApplicationContext .
Это результат любого из этих трех подходов:
Мы видим, что связь была создана правильно, но наш внешний ключ является полем, допускающим значение NULL. Это связано с тем, что оба свойства навигации имеют значение по умолчанию null. Это отношение также называется необязательным отношением (мы говорили об этом в первой части этой статьи).
Если мы хотим создать требуемую связь между сущностями Student и Evaluation , мы должны включить внешний ключ в класс Evaluation . :
Теперь, когда мы выполним нашу миграцию, мы увидим следующий результат:
Очевидно, что наши отношения сейчас необходимы.
Подход с аннотациями данных
Подход с использованием аннотаций к данным содержит только два атрибута, связанных с отношениями. Атрибуты [ForeignKey] и [InverseProperty] .
Атрибут [ForeignKey] позволяет нам определять внешний ключ для свойства навигации в классе модели. Итак, давайте изменим класс Evaluation , добавив этот атрибут:
Мы применили атрибут [ForeignKey] поверх свойства StudentId (которое является внешним ключом в этом классе), присвоив ему имя свойства навигации Student . Но работает и наоборот:
Атрибут ForeignKey принимает один параметр строкового типа. Если внешний ключ является составным ключом, атрибут ForeignKey должен выглядеть следующим образом:
[ForeignKey («Свойство1», «Свойство2»)] .
Какой бы способ мы ни выбрали, результат будет таким же, как и при подходе «по соглашению». Мы собираемся создать требуемую связь между этими двумя таблицами:
Подход Fluent API для конфигурации One-to-Many
Чтобы создать отношение «один ко многим» с этим подходом, нам нужно удалить атрибут [ForeignKey] из класса Evaluation и изменить StudentConfiguration , добавив этот код:
С помощью такого кода мы сообщаем EF Core, что наша сущность Student (объект построителя имеет тип) могут быть связаны со многими объектами Evaluation . Мы также заявляем, что Evaluation находится во взаимосвязи только с одной сущностью Student . Наконец, мы предоставляем информацию о внешнем ключе в этой связи.
Результат будет таким же:
Здесь нужно упомянуть одну вещь.
Для модели базы данных, такой как мы определили, нам не нужен метод HasForeignKey . Это потому, что свойство внешнего ключа в классе Evaluation имеет тот же тип и то же имя, что и первичный ключ в классе Student. Это означает, что по Конвенции это отношение все равно будет обязательным. Но если бы у нас был внешний ключ с другим именем, например StudId, тогда понадобился бы метод HasForeignKey , потому что в противном случае ядро EF создало бы необязательную связь между классами Evaluation и Student.
Конфигурация отношений Many-to-Many (многие-ко-многим)
Это реализация версии 3.1 EF Core. Это справедливо для EF Core версии 5, но в версии 5 это можно было бы сделать немного иначе. Мы объясним это в следующем разделе.
Прежде чем мы начнем объяснять, как настроить эту связь, давайте создадим необходимые классы в проекте Entities :
Теперь мы можем изменить классы Student и Subject , предоставив свойство навигации для каждого класса по направлению к классу StudentSubject :
В Entity Framework Core мы должны создать объединяющуюся сущность для объединяемой таблицы (StudentSubject). Этот класс содержит внешние ключи и свойства навигации из классов Student и Subject . Кроме того, классы Student и Subject имеют свойства навигации ICollection по отношению к классу StudentSubject . Таким образом, отношения «многие ко многим» - это всего лишь два отношения «один ко многим».
Мы создали наши сущности, и теперь нам нужно создать необходимую конфигурацию. Для этого давайте создадим класс StudentSubjectConfiguration в папке Entities/Configuration:
Как мы уже говорили, многие-ко-многим - это всего лишь две взаимосвязи EF Core «один ко многим», и это именно то, что мы настраиваем в нашем коде. Мы создаем первичный ключ для таблицы StudentSubject , который в данном случае является составным ключом. После настройки первичного ключа мы используем знакомый код для создания конфигураций отношений.
Теперь нам нужно изменить метод OnModelBuilder в классе ApplicationContext :
После этих изменений мы можем создать миграцию и применить ее:
PM> Add-Migration ManyToManyRelationship
Отличная работа. Давай продолжаем.
По сути, класс Student должен иметь public ICollection Subjects , а класс Subject должен иметь public ICollection Students свойство. Нет необходимости ни в третьем классе, ни в свойствах навигации для этого класса.
Но если вы хотите изначально заполнить данные для таблиц Student и Subject и заполнить третью таблицу идентификаторами обоих таблиц, вам придется использовать реализацию, которую мы использовали для версии 3.1.
Метод OnDelete
Метод OnDelete настраивает действия удаления между реляционными сущностями. Мы можем добавить этот метод в конец конфигурации отношений, чтобы решить, как будут выполняться действия удаления.
В методе OnDelete можно использовать следующие значения:
- Restrict - действие удаления не применяется к зависимым объектам. Это означает, что мы не можем удалить основную сущность, если у нее есть связанная зависимая сущность.
- SetNull - зависимая сущность не удаляется, но для ее свойства внешнего ключа установлено значение null.
- ClientSetNull - если EF Core отслеживает зависимую сущность, ее внешний ключ имеет значение null, и эта сущность не удаляется. Если он не отслеживает зависимую сущность, то применяются правила базы данных.
- Cascade - зависимая сущность удаляется вместе с основной сущностью.
Мы также можем видеть это из кода в нашем файле миграции:
Мы можем изменить этот тип поведения, изменив код конфигурации в классе StudentConfiguration :
Давайте создадим еще один перенос:
PM> Добавление миграции StudentEvaluationRestrictDelete
И взгляните на сгенерированный код миграции:
Заключение
Настройка взаимосвязей EF Core в нашей модели базы данных - очень важная часть процесса моделирования.
Мы увидели, что EF Core предлагает нам несколько способов добиться этого и максимально упростить процесс.
Теперь, когда мы знаем, как устанавливать отношения в нашей базе данных, мы можем перейти к следующей статье, где узнаем, как получить доступ к данным из БД.
Надо ли реализовывать паттерны "Unit of Work" и "Repository" если вы используете EntityFramework Core?
Вопрос о необходимости использования паттернов при использовании EntityFramework Core очень частно возникает на просторах сообщества разработчиков на платформе NET, и не только. Ответ простой - и "да", и "нет".
Вот несколько аргументов в пользу того, чтобы отказаться от реализации паттернов Unit of Work и Repository при использовании EntityFramework Core:
- EntityFramework Core изолирует ваш код от базы данных (от любой базы данных)
- DbContext работает по принципу работы паттерна Unit of Work
- DbSet работает по принципу работы паттерна Repository
- EntityFramework Core имеет все возможности для Unit-тестирования (в том числе и без использования паттерна Repository)
А вот несколько контрагрументов в ответ на аргументы из предыдущего параграфа:
- EntityFramework Core не дает возможности получить "из коробки" фильтрованные данные для разбития на страницы (paging), то есть нет методов типа GetPagedList<T> или GetPagedListAsync<T>
- EntityFramework Core не имеет возможности выдавать данные по запросы на основании ролей (прав доступа или разрешений) при выборки из базы данных, то есть вам так или иначе придется реализовывать выборки на основании ролей в каком-то слое (вы можете его называть как угодно, некоторые его называют Unit of Work).
- EntityFramework Core реализует доступ к данным, но никоим образом не призван реализовывать механизмы, обслуживающие ваши бизнес-процессы. Реализация бизнес-процессов, обычно начинается на уровне Repository и продолжается на уровне Unit of Work. Значит вам придется базовые принципы реализовывать самостоятельно (принципы доступа, трансформации, агрегации и т.д.). А это уже, так или иначе, Unit of Work, ну или какая-то его часть.
Что касается, то я использую Unit of Work и Repository и не просто, а реализую в них логику по разбитию на страницы выдаваемых результатов, а также логику выборке данных с учетом ролей (roles or permissions). Безусловно, вы можете создать свой уровень доступа к данным, назвать каким угодно названием и делать вид, что это не Unit of Work. :)
Читайте также: