Поставщики типов F# и интерфейсы C# + Entity Framework

Вопрос очень технический, и он лежит глубоко между различиями F# и C#. Вполне возможно, что я что-то упустил. Если вы обнаружите концептуальную ошибку, пожалуйста, прокомментируйте, и я обновлю вопрос.

Начнем с мира C#. Предположим, что у меня есть простой бизнес-объект, назовем его Person (но, пожалуйста, имейте в виду, что в той бизнес-области, с которой мы работаем, существует более 100+ объектов, намного более сложных):

public class Person : IPerson
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

и я использую DI / IOC, поэтому я никогда не передаю Person. Скорее, я бы всегда использовал интерфейс (упомянутый выше), назовите его IPerson:

public interface IPerson
{
    int PersonId { get; set; }
    string Name { get; set; }
    string LastName { get; set; }
}

Бизнес-требование состоит в том, чтобы человека можно было сериализовать в/десериализовать из базы данных. Допустим, я решил использовать для этого Entity Framework, но фактическая реализация кажется не относящейся к вопросу. На данный момент у меня есть возможность ввести классы, связанные с «базой данных», например. EFPerson:

public class EFPerson : IPerson
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

вместе с соответствующими атрибутами и кодом, связанным с базой данных, которые я пропущу для краткости, а затем используйте Reflection для копирования свойств интерфейса IPerson между Person и EFPerson ИЛИ просто используйте EFPerson (передается как IPerson) напрямую ИЛИ сделайте что-то еще. Это совершенно не имеет значения, так как потребители всегда будут видеть IPerson, и поэтому реализация может быть изменена в любое время, а потребители ничего об этом не знают.

Если мне нужно добавить свойство, то я сначала обновлю интерфейс IPerson (допустим, я добавлю свойство DateTime DateOfBirth { get; set; }), а потом компилятор подскажет, что исправить. Однако если я уберу свойство из интерфейса (допустим, LastName мне больше не нужен), то компилятор мне не поможет. Однако я могу написать тест на основе Reflection, который бы гарантировал идентичность свойств IPerson, Person, EFPerson и т. д. Это на самом деле не нужно, но это можно сделать, и тогда это будет работать как по волшебству (и да, у нас есть такие тесты, и они работают как по волшебству).

Теперь давайте перейдем к миру F#. Здесь у нас есть провайдеры типов, которые полностью избавляют от необходимости создавать объекты базы данных в коде: они создаются автоматически провайдерами типов!

Прохладный! Но так ли это?

Во-первых, кто-то должен создавать/обновлять объекты базы данных, и если задействовано более одного разработчика, то естественно, что база данных может и будет обновляться/понижаться в разных ветках. До сих пор, по моему опыту, это очень мучительно, когда задействованы поставщики типов F#. Даже если C# EF Code First используется для обработки миграций, требуются некоторые «обширные шаманские танцы», чтобы сделать поставщиков типов F# «счастливыми».

Во-вторых, в мире F# по умолчанию все неизменяемо (если только мы не сделаем его изменяемым), поэтому мы явно не хотим передавать изменяемые объекты базы данных вверх по течению. Это означает, что как только мы загрузим изменяемую строку из базы данных, мы хотим как можно быстрее преобразовать ее в «родную» неизменяемую структуру F#, чтобы работать только с чистыми функциями вышестоящего уровня. Ведь использование чистых функций уменьшает количество необходимых тестов, наверное, в 5-50 раз, в зависимости от предметной области.

Вернемся к нашему Person. Я пока пропущу любое возможное переназначение (например, целое число базы данных в случае F# DU и тому подобное). Итак, наш F# Person будет выглядеть так:

type Person =
    {
        personId : int
        name : string
        lastName : string
    }

Так вот, если «завтра» мне нужно добавить в этот тип dateOfBirth : DateTime, то компилятор мне расскажет обо всех местах, где это нужно исправить. Это здорово, потому что компилятор C# не скажет мне, куда мне нужно добавить эту дату рождения, … кроме базы данных. Компилятор F# не скажет мне, что мне нужно пойти и добавить столбец базы данных в таблицу Person. Однако в C#, поскольку мне пришлось бы сначала обновить интерфейс, компилятор сообщит мне, какие объекты должны быть исправлены, включая объекты базы данных.

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

Итак, вот два вопроса.

  1. Как я могу легко управлять миграцией базы данных вверх/вниз в мире F#? И, для начала, как правильно выполнить миграцию базы данных в мире F#, когда задействовано много разработчиков?

  2. Каков способ F# для достижения «лучшего из мира C#», как описано выше: когда я обновляю F#, набираю Person, а затем исправляю все места, где мне нужно добавить/удалить свойства в запись, что будет наиболее подходящим способом F# для «сбой» либо во время компиляции, либо, по крайней мере, во время тестирования, когда я не обновил базу данных, чтобы она соответствовала бизнес-объекту (объектам)?

Я считать ответом на оба вопроса является использование EF в проекте библиотеки C#. Поставщики типов F# действительно хороши для небольших баз данных и проектов. Пока еще нет инструментов для поставщиков типов F# для обработки миграции схемы «100+ гораздо более сложных объектов» в большой команде.

Wallace Kelly 21.01.2019 20:15

Я только узнаю об этом сам, поэтому я не могу дать полный или даже хороший ответ, но совет из моего текущего ресурса: «Если вы должен используете EF, я рекомендую создать уровень С# для вашей модели объекта и получить доступ к этому из фа-диез». - Исаак Абрахам. Кстати, отличный, хорошо написанный вопрос!

ChiefTwoPencils 21.01.2019 21:10
which completely remove the need to create database objects in the code: they are created automatically by the type providers если бы только! Это было обещанием ORM. Это никогда не работает. Вот почему люди говорят о несоответствие объектно-реляционного импеданса. объекты или записи, схема/система типов Приложения редко подходит в качестве схемы базы данных и наоборот.
Panagiotis Kanavos 22.01.2019 08:38

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

Panagiotis Kanavos 22.01.2019 08:41

Поставщики типов F# находятся на более легком конце ORM, подобно микроORM, таким как Dapper. Там нет схем и миграций, только запросы, которые автоматически сопоставляются с объектами. Возвращенные записи могут быть доступны только для чтения для всех, кто заботится, microORM не пытается отслеживать изменения.

Panagiotis Kanavos 22.01.2019 08:43

Как упомянул @PanagiotisKanavos, вы страдаете от несоответствия импеданса. Вы не можете обойти это несоответствие, вместо этого примите его. TP — это вариант, если вы видите обновление БД как отдельное. Обновите схему как 1-й этаж, домен как 2-й.

Devon Burriss 06.02.2019 06:42
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
14
6
903
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

How can I easily manage database up / down migrations in F# world? And, to start from, what is the proper way to actually do the database migrations in F# world when many developers are involved?

Самый естественный способ управления миграцией базы данных — использовать инструменты, встроенные в базу данных, то есть простой SQL. В нашей команде мы используем пакет дбуп, для каждого решения мы создаем небольшой консольный проект, чтобы выполнить миграцию БД в процессе разработки и во время развертывания. Потребительские приложения создаются как на F# (поставщики типов), так и на C# (EF), иногда с одной и той же базой данных. Работает как шарм.

Вы упомянули EF Code First. Все поставщики F# SQL по своей сути являются "Db First", поскольку они генерируют типы на основе внешнего источника данных (базы данных), а не наоборот. Я не думаю, что смешивание двух подходов — хорошая идея. На самом деле я бы не рекомендовал EF Code First для кто-нибудь для управления миграциями: простой SQL проще, не требует «обширных шаманских танцев», бесконечно более гибок и понятен гораздо большему количеству людей. Если вам не нравится ручное создание сценариев SQL и вы можете использовать EF Code First только для автоматического создания сценария миграции, то даже Дизайнер MS SQL Server Management Studio может сгенерировать для вас сценарии миграции

What is the F# way to achieve “the best of C# world” as described above: when I update F# type Person and then fix all places where I need to add / remove properties to the record, what would be the most appropriate F# way to “fail” either at compile time or at least at test time when I have not updated the database to match the business object(s)?

Мой рецепт следующий:

  • Не используйте интерфейсы. как ты сказал :)

interfaces, it just does not feel the F# way

  • Не допускайте утечки автоматически сгенерированных типов из поставщика типов за пределы тонкого уровня доступа к базе данных. Это не бизнес-объекты, а ни один объект EF не по сути.
  • Вместо этого объявите записи F# и/или размеченные объединения в качестве объектов домена. Моделируйте их по своему усмотрению и не чувствуйте себя ограниченным схемой db.
  • На уровне доступа к БД сопоставьте автоматически сгенерированные типы БД с типами F# вашего домена. Каждое использование типов, автоматически сгенерированных поставщиком типов, начинается и заканчивается здесь. Да, это означает, что вам нужно писать сопоставления вручную и учитывать человеческий фактор, например. вы можете случайно сопоставить Имя с Фамилией. На практике это крошечные накладные расходы, и преимущества развязки перевешивают их на величину.
  • Как убедиться, что вы не забыли отобразить какое-то свойство? Это невозможно, компилятор F# выдаст ошибку, если запись не полностью инициализирована.
  • Как добавить новое свойство и не забыть его инициализировать? Начните с кода F#: добавьте новое свойство в запись/записи домена, компилятор F# проведет вас ко всем экземплярам записи (обычно только к одному) и заставит вас инициализировать его чем-то (вам нужно будет добавить сценарий миграции/схему базы данных обновления соответственно ).
  • Как удалить свойство и не забыть очистить все до схемы БД. Начните с другого конца: удалите столбец из базы данных. Все сопоставления между типами поставщика типов и записями домена F# будут нарушены и будут выделены свойства, которые стали избыточными (что более важно, это заставит вас дважды проверить, действительно ли они избыточны, и пересмотреть свое решение).
  • Фактически, в некоторых сценариях вы можете захотеть сохранить столбец базы данных (например, для исторических целей/аудита) и удалить свойство только из кода F#. Это всего лишь один (и довольно редкий) из множества сценариев, когда удобно иметь модель предметной области отделенной от схемы БД.

Короче

  • миграция через простой SQL
  • типы доменов объявляются вручную в записях F#
  • ручное сопоставление поставщиков типов с типами домена F#

Еще короче

Придерживайтесь принципа единой ответственности и наслаждайтесь преимуществами.

оффтоп, если ваша база данных - MS SQL, я рекомендую взглянуть на этот поставщик типов: fsprojects.github.io/FSharp.Data.SqlClient он работает только с MS SQL и требует написания SQL-запросов или сохраненных процессов вручную, т.е. это не совсем ORM, но уровень безопасности времени компиляции не похож на то, что я чувствовал до. Когда он компилируется, он обычно работает, иногда я даже не пытаюсь запустить его, прежде чем двигаться дальше.

KolA 18.08.2019 19:45

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

Konstantin Konstantinov 09.09.2019 22:57

Другие вопросы по теме