MiniQL: создание языка запросов в TypeScript
Изобретать велосипед - значит перестраивать то, что уже было построено раньше. Его коннотация часто бывает негативной - то есть изобретать что-то заново бессмысленно и пустая трата времени, потому что кто-то уже делал это раньше.
В общем, это хорошее предупреждение. Вам следует быть осторожным, когда вы вкладываете свое время и внимание. Но недавно у меня был опыт воссоздания популярной технологии.
Я практически перестроил GraphQL.
В этом сообщении в блоге я хотел бы познакомить вас с моим новым проектом с открытым исходным кодом под названием MiniQL. Мы также поговорим о том, почему я заново изобрел колесо и когда это нормально.
Прямо сейчас
Не терпится взглянуть на MiniQL? Хотите взглянуть на него, а затем вернуться к этому сообщению в блоге? Ознакомьтесь с интерактивным примером здесь. Вы можете найти код для MiniQL на GitHub.
Почему именно MiniQL?
Я бы очень хотел использовать GraphQL. Я использовал его раньше, когда работал в других компаниях. Но, к сожалению, он не совсем соответствовал потребностям моего стартапа (подробнее об этом скоро), поэтому я не смог его использовать.
Я потратил несколько месяцев на то, чтобы интегрировать GraphQL в наш проект. Я знал, что это упростит многие вещи и создаст целостную модель данных, но просто не мог приспособить ее.
Какое-то время зародилась идея, что если бы я мог просто воспроизвести некоторые части GraphQL, я мог бы получить то, что мне нужно. Формировалась концепция MiniQL.
Что такое GraphQL?
GraphQL, если вы еще не слышали о нем, - это язык и среда выполнения для описания, запроса и управления вашими данными. Он очень популярен, имеет открытый исходный код и был создан Facebook.
GraphQL не привязан к какой-либо конкретной базе данных или формату данных и поддерживается многими языками программирования.
Что не так с GraphQL?
Если GraphQL действительно такой классный, то почему я просто не использовал его?
Что ж, для GraphQL определенно есть время и место. Я видел, как он действительно хорошо работает в продакшене. Но для моего стартапа это не сработало.
Основная проблема заключалась в необходимости создания схемы данных. У нас ее еще не было, и я не хотел ее создавать. Мы - стартап, который быстро экспериментирует, чтобы развивать наш продукт и находить правильное решение для рынка. Использование схемы во время быстрой эволюции продукта создает много хлопот.
Схема требует постоянных обновлений, чтобы поддерживать ее актуальность - не говоря уже о постоянных миграциях данных для приведения данных в соответствие со схемой, а также управления отдельными схемами для разных сред. Создание схемы в первую очередь (для наших 20-ти микросервисов) само по себе является сложной задачей.
Поэтому мы сказали: нет, спасибо за схему - она работает против нашего быстрого темпа разработки.
Есть и другие проблемы. GraphQL добавляет дополнительный язык к нашему набору технологий. Это означает дополнительное обучение для наших разработчиков и дополнительную сложность нашего стека. Время, необходимое для интеграции и внедрения GraphQL, лучше потратить на добавление функций для наших клиентов.
Кроме того, это относительно небольшая вещь, но мне очень утомительно, что я должен указывать все поля в данных, которые должны быть возвращены. Я знаю, что в GraphQL это оптимизация для минимизации данных, возвращаемых интерфейсу. Это отлично подходит для оптимизации, но плохо для исследования данных.
Иногда мне просто хочется изучить данные и увидеть их все. Или, может быть, мне просто нужно быстро что-то сделать в MVP или прототипе, и я хочу получить все данные, не задумываясь об этом. Конечно, мне нужно будет сделать оптимизацию позже, но пока мне нужен быстрый и легкий путь.
Почему я не могу просто взять лучшее из обоих миров? В некоторых ситуациях я хотел бы получить все свои данные; в других случаях я хотел бы получить оптимальный или минимизированный набор данных. Для моего стартапа порог входа в GraphQL оказался выше, чем ценность, которую он дает.
Зачем изобретать велосипед?
Хотя изобретение велосипеда часто рассматривается как уничижительный, бывают случаи, когда это хорошо.
Для начала подумайте: если бы никто ничего не изобретал заново, у нас не было бы ничего лучше.
Мы все застряли бы в одних и тех же устаревших старых языках программирования, базах данных и фреймворках. Переосмысление необходимо для инноваций и эволюции - иногда даже для революции.
Но, к сожалению, мы можем увидеть это только задним числом. Только тогда, когда переосмысление окажется успешным (учитывая, что они часто бывают неудачными), вас будут хвалить. А до этого вас наверняка раскритикуют. Чтобы добиться успеха, нужно избавиться от тех, кто говорит вам не изобретать велосипед.
Вот несколько веских причин изобрести велосипед:
- Существующее колесо работает не так, как вы хотите
- Вам нужно колесо получше
- Для развлечения или образования
Я создал MiniQL, потому что GraphQL не работал так, как я хотел.
Лично я не думаю, что смогу сделать что-то лучше GraphQL; он уже очень хорош, и для его улучшения потребуются команда и ресурсы, которых у нас нет. Так что это не было моей мотивацией. Но это отличный повод что-то изобретать заново, потому что вам нужна лучшая версия.
Конечно, также совершенно нормально изобретать что-то для развлечения или обучения, и это фантастический способ узнать что-то новое и получить опыт в качестве разработчика.
Только будьте осторожны, не зайдите так глубоко, чтобы не утонуть. Восстановить сложную технологию - нелегкая задача. Это может быть очень сложно, и чаще всего это не увенчается успехом.
Это не относится к MiniQL. Он уже успешен - по крайней мере, для моего стартапа. Он делает то, для чего был предназначен.
MiniQL небольшой и простой. Я сделал это специально и знал, что это достижимо. Взять только те части GraphQL, которые нам нужны (настроены для нашего конкретного случая использования), и просто отбросить те части GraphQL, которые слишком громоздки или нам не нужны.
Если вы обнаружите, что слишком глубоко погружаетесь, пожалуйста, просто бросьте свой проект. Программируя для хобби или для образования, просто прекратите программировать, как только вы повеселитесь или научитесь чему-то полезному.
Начать проект, обнаружить его очень сложно, а потом отказаться от него - это нормально. Просто убедитесь, что вы получаете что-то взамен, будь то развлечение, обучение, что-то для вашего портфолио или какой-то другой результат.
Но, пожалуйста, не ходите и не изобретайте что-то без надобности в рабочее время! Если вы собираетесь делать это на работе, вам лучше иметь вескую причину и поддержку вашего менеджера, иначе все станет неловко.
Знакомство с MiniQL
MiniQL (как он стал известен) зарождался в моей голове большую часть 2020 года. Я знал, что мне нужен какой-то способ упростить управление данными и их извлечение для моего стартапа. В один из выходных я сел и начал писать код.
MiniQL родился за один уик-энд. Он разработан на TypeScript для использования как в JavaScript, так и в TypeScript. У меня было хорошее представление о том, как это будет работать (после того, как я обдумывал это в течение нескольких месяцев и делал обильные записи). У меня был приоритет, чтобы он оставался небольшим и управляемым. Я с самого начала использовал разработку через тестирование (TDD).
MiniQL потребовались еще одни выходные, чтобы конкретизировать набор функций и стабилизировать основную логику. Базовый механизм запросов был завершен довольно быстро, и с тех пор он работал с перерывами, поскольку я работал над плагинами, примерами и документацией. Интересно отметить, что теперь я потратил гораздо больше времени на примеры и документацию, чем на ядро механизма запросов.
Код довольно абстрактный, хотя его было несложно разработать с помощью итераций TDD. На каждом этапе я добавлял функции или проводил рефакторинг, но ни разу не допустил появления ошибок. Вот как работает TDD: он добавляет к вашему проекту каркас, который поддерживает и защищает вас во время кодирования.
TDD действительно поддерживает абстрактный характер кода, что затрудняет его чтение. Абстрактный код также затрудняет объяснение, отсюда и работа над примерами и документацией.
Вы можете найти механизм запросов MiniQL на GitHub. Для быстрого интерактивного обзора MiniQL см. Интерактивный пример здесь.
Если вы новичок в разработке и хотите увидеть пример того, как работает TDD, это отличный проект для этого. Вы можете путешествовать во времени и воспроизводить историю этого проекта от коммитов к коммитам, чтобы увидеть, как он развивается с течением времени (на самом деле, я сделал покадровое видео первых нескольких часов разработки).
Не стесняйтесь работать над этим, получая каждую фиксацию по очереди и запуская тесты, чтобы увидеть, какие тесты были добавлены и как они выполняют код. Это отличный способ получить представление об итерационном характере разработки, основанной на тестировании.
Коротко о MiniQL
Рисунок 1 ниже кратко описывает MiniQL.
Выполняем JSON-запросы к MiniQL (1). Определяемый пользователем преобразователь запросов отвечает за получение данных из любой базы данных или источника данных, которые мы используем (2). Резолвер - это реализация шаблона адаптера - он адаптирует выбранный нами источник данных для работы с MiniQL.
Затем MiniQL организует полученные данные в соответствии с запросом (4) и возвращает отформатированный документ JSON, содержащий запрошенные данные (5).
Цели
MiniQL стремится быть крошечным, но очень гибким.
В первую очередь, мы хотим иметь возможность легко создавать и выполнять запросы. Должна быть возможность отправлять запросы и получать результаты по сети. Вот почему и запросы, и результаты выражаются в формате JSON.
JSON уже распространен во многих технических стеках, поэтому это означает, что мы не представляем новый язык. Это также означает, что и запросы, и результаты легко сериализуемы, что отлично, если вам нужно реализовать кеширование.
Также должно быть легко реализовать преобразователь запросов для MiniQL. Мы хотели бы иметь возможность легко подключать любую настраиваемую базу данных или формат данных, по которым мы хотели бы выполнять запросы. Фактически, в MiniQL уже есть несколько встроенных преобразователей, которые скоро появятся (ссылки на примеры см. На странице GitHub). Но создать собственный преобразователь запросов несложно.
MiniQL делегирует функции преобразователю запросов. Делегирование функций преобразователю также означает, что мы можем воспользоваться любыми специальными функциями, предлагаемыми нашей конкретной базой данных.
Одним из основных драйверов для MiniQL была необходимость упростить REST API в продукте моего стартапа. Вместо того, чтобы иметь сотни конечных точек в REST API, я хотел иметь только несколько.
MiniQL позволяет направлять множество типов запросов и операций обновления через одну конечную точку. Это означает, что мы можем значительно сократить количество конечных точек, необходимых нашему приложению. Это также упрощает наш код и дает нам единое место для применения аутентификации и других мер безопасности.
Мы также хотели бы иметь возможность извлекать оптимизированные или минимальные данные на внешний интерфейс (точно так же, как GraphQL), но кроме того, имеет смысл просто извлекать все данные всякий раз, когда нам нужна эта возможность. Получение всех данных полезно для изучения наших данных или в тех случаях, когда нам не нужно или мы не хотим оптимизировать результаты (например, когда мы создаем прототип MVP).
И, наконец, что очень важно, MiniQL не навязывает ненужную структуру или правила нашим данным. MiniQL не имеет встроенной концепции схемы.
Конечно, это не значит, что мы не можем создавать свои собственные правила или структуру. Лично мне нравится использовать TypeScript для наложения системы статической типизации моих данных. Если вы хотите наложить схему поверх MiniQL, вы можете легко использовать схему JSON. Вы можете увидеть пример этого в редакторе запросов интерактивного примера.
MiniQL запросы
По своей сути MiniQL предназначен для получения и обновления объектов в нашем хранилище данных. Мы можем получить одну сущность, все сущности или найти набор сущностей. Мы также можем разрешить вложенные сущности. Благодаря простым комбинациям мы получаем большую гибкость в том, как структурируются результаты нашего запроса. В этом сообщении блога я только показываю, как получать объекты из нашего источника данных. В одном из будущих постов блога я покажу, как обновлять сущности.
Давайте рассмотрим несколько простых примеров извлечения сущностей из набора данных вселенной «Звездных войн».
Получение Дарта Вейдера
Запрос в листинге 1 показывает, как получить запись для одного объекта: Дарта Вейдера.
Запросы MiniQL написаны в JSON, но для этих примеров (а также в интерактивном примере) я демонстрирую использование JSON5, который является более ориентированной на человека версией JSON, которая поддерживает комментарии.
Обратите внимание, как мы используем поле args
для установки имени извлекаемого объекта. Значение args
полностью зависит от распознавателя запросов. Итак, что вам нужно передать здесь, действительно зависит от распознавателя. В этом случае мы ищем запись на основе столбца имени в CSV-файле, и поэтому мы устанавливаем поле name
в запросе на "Darth Vader"
.
Листинг 1: Получение единой сущности
{
"get": {
"character": { // Получает сущность символа.
"args": {
"name": "Darth Vader" // Получает Дарта Вейдера по имени.
}
}
}
}
Листинг 1: результат запроса
{
"character": {
"name": "Darth Vader", // Получает Дарта Вейдера
"height": 202,
"mass": 136,
"hair_color": "none",
"skin_color": "white",
"eye_color": "yellow",
"birth_year": "41.9BBY",
"gender": "male",
"homeworld": "Tatooine",
"species": "Human"
}
}
Получение всех персонажей
Запрос в листинге 2 показывает, как получить всех персонажей из вселенной Звездных войн. Обратите внимание, что мы оставили поле args
пустым. Отсутствие указания аргументов для соответствия какому-либо конкретному символу указывает преобразователю запросов, что мы хотели бы получить все символы вместо определенного символа.
Листинг 2: Получение всех сущностей
{
"get": {
"character": { // Получает сущность символа.
"args": {
// No args, получает все символы.
}
}
}
}
Листинг 2: результат запроса
{
"character": [ // Получает всех персонажей "Звездных войн".
{
"name": "Luke Skywalker",
"height": 172,
"mass": 77,
"hair_color": "blond",
"skin_color": "fair",
"eye_color": "blue",
"birth_year": "19BBY",
"gender": "male",
"homeworld": "Tatooine",
"species": "Human"
},
{
"name": "C-3PO",
"height": 167,
"mass": 75,
"hair_color": "NA",
"skin_color": "gold",
"eye_color": "yellow",
"birth_year": "112BBY",
"gender": "NA",
"homeworld": "Tatooine",
"species": "Droid"
},
// ... и так далее
]
}
Получение вложенной сущности
MiniQL поддерживает получение связанных сущностей и вложение результата в вывод.
Запрос в листинге 3 показывает, как получить персонажа Дарта Вейдера, а также извлекает детали его родного мира как вложенной сущности. Обратите внимание, как это поле resolve
используется для указания того, что мы хотели бы также разрешить детали домашнего мира Вейдера.
Листинг 3: Получение вложенной сущности
{
"get": {
"character": {
"args": {
"name": "Darth Vader" // Получает Дарта Вейдера
},
"resolve": {
"homeworld": { // Получает родной мир Дарта Вейдера.
}
}
}
}
}
Листинг 3: результат запроса
{
"character": {
"name": "Darth Vader", // Получает Дарта Вейдера
"height": 202,
"mass": 136,
"hair_color": "none",
"skin_color": "white",
"eye_color": "yellow",
"birth_year": "41.9BBY",
"gender": "male",
"homeworld": { // Получает родной мир Дарта Вейдера.
"name": "Tatooine",
"rotation_period": 23,
"orbital_period": 304,
"diameter": 10465,
"climate": "arid",
"gravity": "1 standard",
"terrain": "desert",
"surface_water": 1,
"population": 200000
},
"species": "Human"
}
}
Создание преобразователя запросов
Теперь, когда мы можем делать несколько простых запросов, пора научиться создавать преобразователь запросов. Существуют различные плагины MiniQL, которые реализуют преобразователи запросов для некоторых типов источников данных, но в соответствии с их целями, довольно легко создать такой для себя с помощью MiniQL.
Приведенные ниже примеры основаны на использовании MongoDB в качестве источника данных, но для вашей реализации вы можете использовать любую другую базу данных или формат данных, который вы предпочитаете.
Преобразователь запросов для символьной сущности
Преобразователь запросов в листинге 4 показывает простой преобразователь для сущности character
, использованной в предыдущем примере. Обратите внимание, как один преобразователь используется для получения либо одного символа, либо всех символов в зависимости от того, было ли поле name
указано в аргументах запроса.
Весь объект arguments передается напрямую через преобразователь, поэтому наш преобразователь здесь действительно имеет доступ ко всем аргументам, которые передаются из запроса. Это дает возможность передавать настраиваемые аргументы в преобразователь, который можно использовать любым способом, который вы можете себе представить.
Как видите, преобразователи запросов поддерживают асинхронность. Эта функция invoke
является асинхронной, чтобы обеспечить асинхронное извлечение данных из баз данных, файлов, REST API или из любых других источников, из которых вы можете получить свои данные.
Листинг 4: преобразователь запросов для персонажей Звездных войн
const characterCollection = // ... A MongoDB collection.
const queryResolver = {
get: { // Преобразователь для операций "get".
character: { // Распознаватель для символьной сущности.
invoke: async (args, context) => { // Функция для получения символов.
if (args.name !== undefined) {
// Получает один символ.
const character = await characterCollection.findOne({ name: name });
return character;
}
else {
// Получает все символы.
const characters = await characterCollection.find().toArray()
return characters;
}
},
},
},
};
Несколько типов сущностей
Мы можем легко поддерживать несколько типов сущностей, добавляя больше полей в наш преобразователь запросов. В листинге 5 видно, что мы добавили новую сущность species
в наш преобразователь.
Листинг 5: преобразователь запросов для нескольких типов сущностей
const queryResolver = {
get: {
character: { // Resolver for the character entity.
invoke: async (args, context) => {
// ... Получает персонажей из "Звездных войн".
},
},
species: { // Распознаватель для видовой сущности.
invoke: async (args, context) => {
// ... Получает виды Звездных войн.
},
},
},
};
Вложенные сущности
Мы можем поддерживать сущности, размещая их под полем родительской сущности nested
. Например, в листинге 6 мы вложили преобразователь сущностей homeworld
в преобразователь сущностей character
.
Листинг 6: преобразователь запросов для вложенной сущности
const queryResolver = {
get: {
character: {
invoke: async (args, context) => {
// ... Получает персонажей из "Звездных войн".
},
nested: {
homeworld: { // Получает родной мир для персонажа.
invoke: async (args, context) => {
// ... Получает планету Звездных войн.
},
},
},
},
};
MiniQL делегирует преобразователю запросов
MiniQL делегирует все детали извлечения объекта преобразователю запросов. Все, что указано в поле запроса сущности args
, передается преобразователю. Это означает, что вы можете реализовать любые функции запросов, например, текстовый поиск сущностей или раскрытие специальных функций вашей конкретной базы данных.
В качестве примера в листинге 7 показано, как мы можем реализовать разбиение на страницы в нашем преобразователе запросов. Обратите внимание, как этот код ожидает, что поля skip
и limit
будут предоставлены через аргументы запроса. MiniQL не знает и не заботится об этих конкретных полях; они имеют отношение только к тому, как мы реализуем наш преобразователь запросов.
В этом примере значения skip
и limit
передаются драйверу MongoDB для разбивки на страницы набора всех символов.
Листинг 7: преобразователь запросов, реализующий разбиение на страницы
const queryResolver = {
get: {
character: {
invoke: async (args, context) => {
if (args.name !== undefined) {
// Получает один символ.
const character = await characterCollection.findOne({ name: name });
return character;
}
else {
// Получает все символы.
const characters = await characterCollection.find()
.skip(args.skip) // пропускает несколько записей.
.limit(args.limit) // Ограничивает количество возвращаемых записей.
.toArray()
return characters;
}
},
},
},
};
Вывод
MiniQL - это крошечный, но мощный язык запросов на основе JSON, вдохновленный GraphQL. Он нацелен на копирование частей GraphQL, без которых я не могу обойтись, и отбрасывает те части, которые я считаю посторонними, утомительными или ненужными.
MiniQL реализован в TypeScript для использования в JavaScript или TypeScript, но нет причин, по которым он не может быть реализован и для других языков программирования. Входами и выходами MiniQL являются JSON, поэтому язык запросов MiniQL по сути не зависит от языка программирования.
Изобретать колесо часто считается плохим занятием. Но это может быть полезно, а иногда необходимо для прогресса. Мы также можем делать это просто для развлечения или обучения, но помните о причинах, по которым вы это делаете, и о том, какую ценность вы получаете от этого. Если проект «переосмысления» становится слишком сложным, не бойтесь просто бросить его. Если вы повеселились или чему-то научились, это хороший результат, даже если вы не закончили его.
На самом деле выполнение проектов - это тяжелая работа. Чтобы иметь больше шансов завершить проект, сосредоточьтесь на узком круге. Это даст вам лучший шанс закончить его.