Вопрос очень технический, и он лежит глубоко между различиями 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# делается совсем по-другому и обычно достигается за счет передачи функций, а не интерфейсов.
Итак, вот два вопроса.
Как я могу легко управлять миграцией базы данных вверх/вниз в мире F#? И, для начала, как правильно выполнить миграцию базы данных в мире F#, когда задействовано много разработчиков?
Каков способ F# для достижения «лучшего из мира C#», как описано выше: когда я обновляю F#, набираю Person, а затем исправляю все места, где мне нужно добавить/удалить свойства в запись, что будет наиболее подходящим способом F# для «сбой» либо во время компиляции, либо, по крайней мере, во время тестирования, когда я не обновил базу данных, чтобы она соответствовала бизнес-объекту (объектам)?
Я только узнаю об этом сам, поэтому я не могу дать полный или даже хороший ответ, но совет из моего текущего ресурса: «Если вы должен используете EF, я рекомендую создать уровень С# для вашей модели объекта и получить доступ к этому из фа-диез». - Исаак Абрахам. Кстати, отличный, хорошо написанный вопрос!
which completely remove the need to create database objects in the code: they are created automatically by the type providers если бы только! Это было обещанием ORM. Это никогда не работает. Вот почему люди говорят о несоответствие объектно-реляционного импеданса. объекты или записи, схема/система типов Приложения редко подходит в качестве схемы базы данных и наоборот.
База данных не является деталью реализации приложения, на самом деле это часто противоположный с базой данных, пережившей приложение. Большую часть времени база данных обслуживает приложения несколько. Даже в одном и том же приложении база данных обслуживает варианты использования несколько, которые отображаются как разные DbContexts и сопоставления. В таком случае EF Code First и сами миграции базы данных завершатся ошибкой.
Поставщики типов F# находятся на более легком конце ORM, подобно микроORM, таким как Dapper. Там нет схем и миграций, только запросы, которые автоматически сопоставляются с объектами. Возвращенные записи могут быть доступны только для чтения для всех, кто заботится, microORM не пытается отслеживать изменения.
Как упомянул @PanagiotisKanavos, вы страдаете от несоответствия импеданса. Вы не можете обойти это несоответствие, вместо этого примите его. TP — это вариант, если вы видите обновление БД как отдельное. Обновите схему как 1-й этаж, домен как 2-й.





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
Короче
Еще короче
Придерживайтесь принципа единой ответственности и наслаждайтесь преимуществами.
оффтоп, если ваша база данных - MS SQL, я рекомендую взглянуть на этот поставщик типов: fsprojects.github.io/FSharp.Data.SqlClient он работает только с MS SQL и требует написания SQL-запросов или сохраненных процессов вручную, т.е. это не совсем ORM, но уровень безопасности времени компиляции не похож на то, что я чувствовал до. Когда он компилируется, он обычно работает, иногда я даже не пытаюсь запустить его, прежде чем двигаться дальше.
спасибо, я уже использую его и выполняю это ручное сопоставление типа поставщика типов с типами F# в одном проекте. Это происходит естественным образом в F#, поскольку мы абсолютно не хотим, чтобы эти изменяемые, обнуляемые структуры уходили в мир чистых функций :)
Я считать ответом на оба вопроса является использование EF в проекте библиотеки C#. Поставщики типов F# действительно хороши для небольших баз данных и проектов. Пока еще нет инструментов для поставщиков типов F# для обработки миграции схемы «100+ гораздо более сложных объектов» в большой команде.