Prisma ORM недавно выпустила поддержку для MongoDB. Это первый раз, когда Prisma поддерживает базу данных за пределами мира SQL. Prisma известна поддержкой многих реляционных баз данных, но как получилось, что она смогла поддержать совершенно другую базу данных MongoDB?
🕴️ Я работаю в качестве инженерного менеджера в команде Prisma по разработке схем. Мы отвечаем за части Prisma, связанные с управлением схемами, к которым, прежде всего, относятся наши инструменты миграции и интроспекции, а также язык и файл схем Prisma (и наше потрясающее расширение Prisma VS Code!).
Другой большой командой, работающей над объектно-реляционным связующим (ORM) Prisma, является команда Client, которая создает Prisma Client и Query Engine. Они позволяют пользователям взаимодействовать с базой данных для чтения и манипулирования данными.
В этом блоге я кратко рассказываю о том, как наша команда заставила функцию интроспекции схемы Prisma работать для нового коннектора MongoDB, а также об интересных проблемах, которые мы решили на этом пути.
- Prisma
- Prisma + MongoDB
- Prisma и MongoDB: командная редакция схемы
- Интроспекция схемы в MongoDB
- Исследование
- MongoDB Compass
- mongodb-schema.
- Реализация интроспекции для MongoDB в Prisma
- Как представить поля с разными типами
- Как выполнять итерации над данными, чтобы получить более чистую схему
- Как обогатить интроспективную схему Prisma отношениями
- Резюме
Prisma
Prisma – это ORM для баз данных Node.js, построенный вокруг языка схем Prisma и файла схемы Prisma, содержащего абстрактное представление базы данных пользователя. Когда в вашей базе данных есть таблицы со столбцами определенных типов данных, они представляются как модели с полями определенного типа в вашей схеме Prisma.
Prisma использует эту информацию для создания полностью безопасного с точки зрения типов TypeScript/JavaScript Prisma Client, который упрощает взаимодействие с данными в вашей базе данных (то есть он будет жаловаться, если вы попытаетесь записать String
в поле Datetime
, и убедится, что вы, например, включили информацию для всех ненулевых столбцов без значения по умолчанию и тому подобное).
Prisma Migrate использует ваши изменения в схеме Prisma для автоматической генерации SQL, необходимого для миграции вашей базы данных в соответствии с новой схемой. Вам не нужно думать о необходимых изменениях. Вы просто пишете, чего вы хотите добиться, а Prisma затем интеллектуально генерирует SQL DDL (язык определения данных) для этого.
Для пользователей, которые хотят начать использовать Prisma с существующей базой данных, в Prisma есть функция Introspection. Вы вызываете команду CLI prisma db pull
для “извлечения” схемы существующей базы данных, и Prisma автоматически создает для вас схему Prisma, так что ваша существующая база данных может быть использована с Prisma за считанные секунды.
Это работает одинаково для PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB и даже SQLite и основывается на том, что реляционные базы данных довольно похожи, имеют таблицы и столбцы, понимают некоторый диалект SQL, имеют внешние ключи и такие понятия, как ссылочная целостность.
Prisma + MongoDB
Одной из наших самых востребованных функций была поддержка Prisma с MongoDB. Тема запроса на GitHub о поддержке MongoDB с января 2020 года долгое время была самой популярной, набрав в общей сложности более 800 откликов.
MongoDB известна своей гибкой схемой и моделью документов, в которой можно хранить JSON-подобные документы. При моделировании данных MongoDB придерживается парадигмы, отличной от реляционных баз данных – здесь нет таблиц, столбцов, схем или внешних ключей для представления отношений между таблицами. Данные часто хранятся сгруппированными в одном документе со связанными данными или “денормализованными”, что отличается от того, что можно увидеть в реляционной базе данных.
Как же можно объединить эти совершенно разные миры?
Prisma и MongoDB: командная редакция схемы
Для нашей команды это означало выяснить:
- Как представить структуру MongoDB и ее документы в схеме Prisma.
- Как перенести эти структуры данных.
- Как дать людям возможность исследовать их существующую базу данных MongoDB, чтобы они могли легко начать использовать Prisma.
К счастью, решение 1 и 2 оказалось относительно простым:
-
Если в реляционных базах данных есть таблицы, столбцы и внешние ключи, которые отображаются на модели Prisma с их полями и отношениями, то в MongoDB есть эквивалентные коллекции, поля и ссылки, которые могут быть отображены таким же образом. Prisma Client может использовать эту информацию для обеспечения той же безопасности типов и функциональности на стороне клиента.
Реляционная база данных Prisma MongoDB Таблица → Модель ← Коллекция Колонка → Поле ← Поле Внешний ключ → Отношение ← Ссылка -
Поскольку схему базы данных переносить не нужно, создание и обновление индексов и ограничений – это все, что требовалось для изменения схемы базы данных MongoDB. Поскольку нет SQL для изменения структуры базы данных (которая нигде не записана и не определена), Prisma также не пришлось создавать файлы миграции с операторами Data Definition Language (DDL) и можно было просто ограничиться
prisma db push
, чтобы напрямую привести базу данных к желаемому конечному состоянию.
Более серьезной проблемой оказалась функция Introspection.
Интроспекция схемы в MongoDB
В реляционных базах данных со схемой всегда есть способ запросить схему. В PostgreSQL, например, вы можете запросить несколько представлений в схеме information_schema
, чтобы выяснить все детали о структуре базы данных, например, сгенерировать DDL SQL, необходимый для воссоздания базы данных, или абстрагировать ее в схему Prisma.
Поскольку MongoDB имеет гибкую схему (если только схемы не навязываются с помощью функции проверки схемы), не существует такого хранилища информации, к которому можно было бы легко обращаться. Это, конечно, создает проблему, как реализовать интроспекцию для MongoDB в Prisma.
Исследование
Как и любая хорошая инженерная команда, мы начали с … погуглили немного. Нет необходимости изобретать колесо, если кто-то уже решил эту проблему в прошлом. Поиск по запросам “MongoDB introspection”, “MongoDB schema reverse engineering” и (как мы узнали из родного термина) “MongoDB infer schema”, к счастью, принес несколько интересных и стоящих результатов.
MongoDB Compass
Собственный графический интерфейс базы данных MongoDB Compass имеет вкладку “Schema” в коллекции, которая может анализировать коллекцию, чтобы “предоставить обзор типа данных и формы полей в конкретной коллекции”.
Она работает путем выборки 1000 документов из коллекции, в которой есть не менее 1000 документов, анализа отдельных полей и последующего представления их пользователю.
mongodb-schema
.
Еще одним ресурсом, который мы нашли, был репозиторий Лукаса Храбовского mongodb-infer
от 2014 года. Позже в том же году он, похоже, слился/был заменен на mongodb-schema
, который обновляется и по сей день.
Это CLI и библиотечная версия той же идеи – и действительно, при проверке исходного кода MongoDB Compass вы увидите зависимость для mongodb-schema
, которая используется под капотом.
Реализация интроспекции для MongoDB в Prisma
Обычно поиск библиотеки с открытым исходным кодом и лицензией Apache 2.0 означает, что вы только что сэкономили команде инженеров много времени, и команда может просто стать пользователем этой библиотеки. Но в данном случае мы хотели реализовать нашу интроспекцию в том же движке интроспекции, который мы используем и для баз данных SQL, и который написан на языке Rust. Поскольку для Rust пока не существует mongodb-schema
, нам пришлось реализовать это самостоятельно. Зная, как работает mongodb-schema
, это оказалось несложно:
Мы начинаем с того, что просто получаем все коллекции в базе данных. Драйвер MongoDB Rust предоставляет удобный db.list_collection_names()
, который мы можем вызвать, чтобы получить все коллекции, и каждая коллекция превращается в модель для схемы Prisma. 🥂
Чтобы заполнить поля их типом, мы получаем выборку до 1000 случайных записей из каждой коллекции и перебираем их. Для каждой записи мы отмечаем, какие поля существуют, и какой тип данных они имеют. Мы сопоставляем тип BSON с нашими скалярными типами Prisma (и, при необходимости, с собственным типом). Оптимально, если все записи имеют одинаковые поля с одинаковым типом данных, которые легко и просто сопоставить – и все готово!
Часто бывает так, что не все записи в коллекции настолько однородны. Отсутствующие поля, например, ожидаются и эквивалентны значениям NULL
в реляционной базе данных.
Как представить поля с разными типами
Однако различные типы (например, String
и Datetime
) создают проблему: какой тип мы должны поместить в схему Prisma?
🎓 Урок 1: Просто выбрать наиболее распространенный тип данных – не лучшая идея.
В ранней итерации интроспекции MongoDB мы по умолчанию выбирали наиболее распространенный тип и оставляли комментарий с указанием процента встречаемости в схеме Prisma. Идея заключалась в том, что это должно работать большую часть времени и давать разработчику наилучший опыт разработки – чем лучше типы в вашей схеме Prisma, тем больше Prisma может вам помочь.
Но при тестировании мы быстро выяснили, что существует небольшая (но логичная) проблема: каждый раз, когда Prisma Client встречает тип, который не соответствует тому, что ему было сказано в схеме Prisma, он должен выдать ошибку и прервать запрос. В противном случае он будет возвращать данные, которые не соответствуют его собственным сгенерированным типам для этих данных.
Хотя мы знали, что такое может произойти, нам было неясно, как часто это приводит к сбою в работе Prisma Client. Мы быстро убедились в этом при использовании такой схемы Prisma с конфликтующими типами в базовой базе данных с помощью Prisma Studio, встроенного графического интерфейса базы данных, который поставляется вместе с Prisma CLI (просто запустите
npx prisma studio
). По умолчанию она загружает 100 записей модели, которую вы просматриваете – и когда в базе данных из 1000 записей было ~5% записей с другим типом, очень часто случалось, что они попадали уже на первой странице. Prisma Studio (а также приложение, использующее эти схемы) была практически непригодна для таких наборов данных таким образом.
К счастью, все в MongoDB является Document
, что соответствует полю типа Json
в Prisma. Поэтому, когда поле имеет различные типы данных, мы используем вместо него Json
, выводим предупреждение в Prisma CLI и помещаем комментарий над полем в схеме Prisma, которую мы рендерим, содержащий информацию о типах данных, которые мы нашли, и насколько они распространены.
Как выполнять итерации над данными, чтобы получить более чистую схему
Использование Json
вместо конкретного типа данных, конечно, существенно снижает пользу, которую вы получаете от Prisma, и фактически позволяет вам записывать в поле любой JSON (делая данные еще менее однородными и более сложными для обработки со временем!). Но, по крайней мере, вы можете читать все существующие данные в Prisma Studio или в своем приложении и взаимодействовать с ними.
Предпочтительный способ исправить конфликтующие типы данных – прочитать и обновить их вручную с помощью сценария, а затем снова запустить prisma db pull
. Новая схема Prisma должна отображать только один тип, который все еще присутствует в коллекции.
🎓 Урок 2: Выводите типы Prisma в схему Prisma, а не типы MongoDB.
Первоначально мы выводили необработанную информацию о типах, полученную от драйвера MongoDB Rust, – типы BSON – в предупреждения CLI и комментарии к схеме Prisma, чтобы помочь нашим пользователям итеративно просмотреть свои данные и исправить тип. Оказалось, что хотя технически это правильно и говорит пользователю о типе данных, использование имен типов BSON в контексте Prisma сбивает с толку. Мы перешли на вывод имен типов Prisma, и теперь это кажется пользователям гораздо более естественным.
Хотя Prisma рекомендует всем очищать свои данные и минимизировать количество конфликтующих типов, возвращаясь к Json
, это, конечно, тоже правильный выбор.
Как обогатить интроспективную схему Prisma отношениями
Добавив информацию об отношениях в интроспективную схему Prisma, вы можете указать Prisma обрабатывать определенный столбец как внешний ключ и создавать отношение с данными в нем. user User @relation(fields: [userId], references: [id])
создает отношение к модели User
через локальное поле userId
. Итак, если вы используете ссылки MongoDB для отношений модели, добавьте к ним @relation
, чтобы Prisma могла обращаться к ним в Prisma Client, эмулировать ссылочные действия и помогать ссылочной целостности для сохранения чистоты данных.
В настоящее время Prisma не предлагает способа обнаружения или подтверждения потенциальных отношений между различными коллекциями. Мы хотим сначала узнать, как пользователи MongoDB на самом деле используют отношения, а затем помочь им оптимальным способом.
Резюме
Реализация хорошей истории интроспекции для MongoDB была интересным вызовом для нашей команды. Вначале казалось, что два совершенно разных мира сталкиваются друг с другом, но в итоге мы легко нашли правильные компромиссы и решения для достижения оптимального результата для наших пользователей. Мы уверены, что нашли отличную комбинацию, которая сочетает в себе лучшее от MongoDB и то, что люди хотят получить от Prisma.
Попробуйте Prisma и MongoDB с существующей базой данных MongoDB или начните с нуля и создайте ее по ходу дела.