Ошибки выбора MongoDB в качестве основной БД в стартапе
В этой статье я хочу рассказать о своих ошибках, которые я допустил, когда писал сервис, у которого MongoDB была основной БД для хранения пользовательских данных (да и не только, но об этом ниже).
Я ни в коем случае не считаю, что MongoDB это плохая БД и ее не нужно использовать. Более того, я считаю, что только мои кривые руки завели меня в ситуацию, из которой пришлось выходить переписыванием сервиса под другую БД (ушел на Postgres и кайфую).
Тем не менее, нельзя знать всего и чтение документации не спасает от катастроф во время самой реализации проекта. Особенно, если ваши ожидания от инструмента разошлись с реальностью.
На мой взгляд, маркетологи MongoDB приукрасили области применениях БД на своем сайте. MongoDB не универсальная. Далеко не универсальная и даже не пытайтесь на нее смотреть как на решение всех ваших проблем.
Не использовал ORM или хотя бы DTO
Начну с первой пули калибра 155 мм в ногу. Если после чтения этого блока захочется смеяться и говорить, что «ну это же очевидно», то поверьте мне, я не один такой наивный.
Используя MongoDB очень легко можно наговнокодить, если очень торопиться и не тратить время на написание dataclass или DTO.
Когда я начал писать, я взял pymongo. Без всего. Это показалось мне очень удобным: создал клиента, подцепил конфигурацию и вперед. Можно быстро обращаться к коллекциям и писать в них вообще не думая о схемах данных. Безусловно, если вы один разработчик, то в начале вам не о чем беспокоиться. Сервис маленький, вы один. Весь проект помещается в вашу голову. Какие тут могут быть проблемы?
Через месяц-два вы все-таки начнете допускать ошибки в именах полей. Первый раз это будет выглядеть как простая опечатка, которую вы ищете полчаса. «Почему же это поле не записалось?». Ааа, потому что я чудак, вот почему: user_email вместо просто email. Хорошо, если вы это заметите раньше, чем эта ошибка уедет на продакшн и часть данных создастся с неверным полем. Если это случилось: извольте написать миграцию и перенести данные. Опять-таки: хорошо, если это всего лишь простая ошибка в поле и она не связана с типом данных.
Ошибку я попытался исправить поменяв PyMongo на MongoEngine. Спойлер: сильно легче не стало.
Используйте классы или контейнеры для данных. Пишите валидации данных. Все это очевидно, но уже постфактум. И тесты пишите.
Верил, что без миграций и схем данных в MongoDB будет легче
По-началу кажется, что «инкриментить ценность проекта» будет легче без схем БД. Нет, нет и еще раз нет.
Миграции для развивающегося сервиса это не только про переименовывание полей или добавление новых. Это еще и про целостность и понятность данных. В MongoDB можно записать в одну коллекцию самые разные по структуре документы. Они могут быть вообще разные, ни одним полем (ну кроме id) не похожими друг на друга. И это очень большая проблема.
Я уже молчу про то, чтобы сделать быструю DSL на основе схемы данных, потому что…, а нет ее этой вашей схемы данных. Если вам приспичит что-то переделать, то вам придется строить очень сложный запрос (я в итоге честно нагуглил, когда после второго дня сдался), чтобы собрать структуру данных сложного документа, который наполнялся в течение почти года.
Схемы данных — это порядок и дисциплина. Схемы данных — это валидация. Это возможность быстро окинуть данные проекта взглядом и понять как устроен ваша модель или объект. Без схем оооочень больно. Если сюда еще прибавить мою первую ошибку, то представьте как у меня горела задница, когда я переносил все в SQL.
Стрелял в ногу типами данных
Инт или стринг? MongoDB пофигу. У вас может быть два документа с одним и тем же полем, скажем, age, но если на стороне кода приложения ошиблись, то ловите эксепшены. Тысячи эксепшенов и громкий мат из вашего рта среди ночи. Без тестов или DTO в спешке, которая присуща почти любому стартапу, вы будете постоянно ловить эту проблему. А ваша любимая IDE этого может и не заметить (указывайте типы в ваших функциях, ага, знаю).
Думал, что MongoDB быстрая и будет такой всегда
MongoDB действительно может быть быстрой — когда нужно тупо писать и не думать о том, что когда-нибудь придется читать данные или, о боже, делать сложные запросы, где вы захотите сделать aggregate или имитировать join нескольких таблиц коллекций.
Вы где-то читали, что MongoDB экономная в плане потребления ОЗУ? Это вранье. С ростом данных MongoDB будет жрать много ОЗУ. Это зависит сразу от двух факторов: кол-во данных в БД и какого рода операции вы захотите делать.
JOIN это очень дорого, а query по вложенным документам — это гарантия сломать мозги
Авторы MongoDB рекомендуют не использовать монговских JOIN. Он там как бы есть, но он говно. Правильно делать один документ и в нем хранить все связанные данные. Все как бы здорово, но попробуйте потом сделать query по этим самым данным. Сложный query, подразумевающий исключения и логику «или». Это больно, как орбит дверная ручка.
Аналитика — тот еще цирк
Собственно, эта проблема исходит из предыдущей. Посчитать что-то сложнее, чем просто total () — нужно очень сильно заморочиться. Ответить на простые вопросы типа «сколько у нас объектов N для пользователей B, которые входят в категории C, W, Z и делали вот такие-то вещи» в MongoDB превращаются в многоэтажные нечитаемые JSON-подобные запросы, которые трудно поддерживать. Запросы нечитаемые. Можно, конечно же, научить обезьяну курить, но какой в этом смысл? MongoDB не для аналитических данных.
Поиск это тоже цирк с двуногими конями
На сайте MongoDB есть сладкие примеры инвентарок, магазина, коллекций музыки или книг. Они такие классные и такие в вакууме, что в моменте, когда вам нужно реализовать поиск с разными условиями, то волосы на жопе шевелятся. То, что в SQL делается легко даже новичками, в MongoDB превращается в адский ад.
Я тоже думал, что поиск будет легко реализуемым. Таким же простым, как писать запросы на запись без миграций, схем и всего, чем обычно пугают сравнивая SQL и NoSQL базы данных.
Я пробовал уйти от проблем с поиском путем разделения больших документов на отдельные коллекции. Ну знаете, выделить из user его список загруженных документов перенеся их в отдельную коллекцию files. Но это подразумевает, что вам нужно его обратно джойнить, когда он вам потребуется. Но чем больше одновременных запросов к БД, тем ей больнее — потребление ОЗУ растет как на дрожжах.
Поиск по MongoDB это дорого, сложно и имеет никакого сравнения с простотой SQL.
От чего еще болело
Одним списком, чтобы не вставать дважды.
Рецептов и готовых решений мало или вообще нет
Расчеты и встроенные процедуры — забудьте. Без опыта JS это сложнее SQL в разы и дорого
Нормальных бесплатных GUI нет (несмотря на то, что я умею в терминал и люблю консольные приложения). Все глубоко урезанные (привет, Robo3T) и все равно заставляют писать запросы руками, которые вновь подчеркну, далеко не сахар как SQL
Поддержка платная и оооочень дурная (кто пользовался, тот знает)
Так для чего MongoDB реально подходит?
Все-таки инструментом пользуются. Здесь я перечислил то, как я вижу правильное назначение для MongoDB:
Логи. Старые добрые логи, которые пишут потоком из разных сервисов и читают раз в полгода, если случился инцидент.
Данные, по которым вы никогда не будете искать дальше самого верхнего уровня. Максимум: взять какой-нибудь документ по его ID или другому полю (например, user_id, который вы до этого взяли из другой БД).
Данные, которые вы будете писать большую часть времени разом и одним документом. Редко когда придется менять отдельные поля.
Кэш большого объема, для удобства разбитый по отдельным полям. Его быстро можно прочитать, легко инвалидировать. Он в виде структурированного документа может быть сразу использован вашим приложением.
Сессии (хотя вопрос скорости работы все еще остается). Аналогично кэшу. Создали документ, бросили его в MongoDB и взяли через какое-то время одному-двум полям. Без глубокого поиска. Максимально просто. MongoDB, кстати, удобно шардится и реплицируется — это прям реально ее преимущество.
Очереди. Точнее, можно использовать MongoDB как БД для хранения очередей данных для последующей обработки. Аналогично кэшу и сессиям MongoDB дает возможность хранить структурированный документ с данными очереди.
Как видите, прежде всего я пишу о вещах, которые не подразумевают поиска по данным в MongoDB и не будут требовать соединять, агрегировать или как-то еще извращаться.
Итоги
Все ошибки, которые я допустил, умножались на кол-во разработчиков, которые приходили в проект и что-то дописывали от себя. Иногда придерживаясь уже написанного, а иногда добавляя свое творчество в зависимости от своего опыта. В результате ком проблем рос и в какой-то момент добавлять новые фичи и выполнять отладку стало просто невыносимо. С учетом, что это далеко не первый проект, я сам был в шоке от того, как я в этой ситуации оказался.
Отсутствие схемы данных — это не преимущество MongoDB, а громадный недостаток. Невозможность делать нормальные джойны (подчеркиваю: нормальные, а не монговские с конским оверхедом) это тоже проблема, которая не позволяет разбить БД на логические блоки (или, проще говоря, навести порядок). Любой проект можно уложить в любую популярную SQL БД, а вот в MongoDB далеко не все.
Кто-то может сказать, что я неосилятор и вообще RTF. Возможно, но я не хочу бороться и подгонять инструмент, я хочу его использовать и быть уверенным, что он меня не подведет, а я буду точно знать что он может и чего не может. Мне хватает опыта понять, что нет серебряной пули. Думаю, что я повелся на сладкие речи маркетологов MongoDB и решил, что она поможет стартапу быстрее развиваться. Результат: эта статья и +10 к опыту.
Тем не менее, не бойтесь экспериментировать и переписывать. В конце концов, я все равно считаю это хорошим опытом. Благо, мне хватило мозгов писать сам код приложения так, что переход из NoSQL в SQL не занял чресчур много времени и денег. Да и проблему я заметил достаточно рано. Я даже смог переписать все без остановки сервиса, по кусочкам, но это уже другая история.
Главно, если вы будете также как я наивны и поймете, в чем ваша ошибка, поделитесь вашими косяками с окружающими — может быть кто-то уже готов прыгнуть со скалы, но передумает прочитав что-то подобное.