Разберитесь, почему данные, которые сохраняют состояние, создают инерцию, и разработайте стратегии, позволяющие избежать образования технического долга, связанного с базами данных, пишет на портале The New Stack Вэнь Цзе Тео, старший специалист по работе с разработчиками MongoDB.

Технический долг — неизбежный побочный продукт разработки ПО по мере роста сложности. Однако, в отличие от кода приложения без сохранения состояния, присущее базам данных сохранение состояния значительно облегчает накопление этого долга и значительно затрудняет его погашение, иногда требуя недель и даже месяцев планирования и выполнения.

Накопление данных создает огромную инерцию по отношению к изменениям, а наличие разных наборов данных в разных средах, от разработки до производства, затрудняет прогнозирование проблем. Добавьте к этому болезненные, но неизбежные уровни ограничений комплаенса и безопасности на пути к производству, и подъем станет еще круче.

Управление данными для сложных задач — непростая задача, и волшебного решения нет. Но, инвестируя в специальные навыки и анализируя инженерные практики, мы можем предотвратить превращение наших баз данных в «долговые базы».

Приведенные ниже стратегии основаны на моем личном опыте и мнении. Вы можете согласиться с некоторыми из них и не согласиться с другими — и это нормально. В конечном итоге, вы должны сделать наиболее прагматичный выбор, исходя из ваших обстоятельств, а не из общепринятых норм.

Постарайтесь знать базы данных глубже, чем просто поверхностно

Бывало ли у вас такое, что вы изучали какую-то концепцию и жалели, что не применили ее раньше? В коде приложения ошибки или неэффективность часто можно исправить относительно легко. То же самое нельзя сказать об изменениях данных или схемы — или, что еще хуже, о решении заменить всю базу данных.

Знание внутренней структуры выбранной базы данных помогает нам принимать правильные проектные решения относительно схем, индексов и запросов на ранних этапах. Понимание того, как наша база данных обрабатывает параллельный доступ, кэширование, индексирование и выполнение запросов, устраняет необходимость в трудоемком поиске, дорогостоящих исследованиях и разрушительных изменениях в дальнейшем.

Вдобавок ко всему, добавление нового хранилища данных в наш стек обходится очень дорого. Помимо очевидной нагрузки на обслуживание и мониторинг, разработчики сталкиваются с повышенной когнитивной нагрузкой и техническими слепыми зонами, которые являются рассадником ошибок.

Глубокие знания позволяют нам максимально эффективно использовать основную базу данных, прежде чем внедрять другую. В зависимости от наших потребностей в производительности, правильно настроенная база данных может даже исключить необходимость во внешнем кэше-контейнере. Помните: современные базы данных также имеют свои собственные внутренние кэши.

Разработайте свою модель данных (но не переусложните)

Модель данных — это основа того, как выполняются наши запросы. Такие базы данных, как MongoDB, Neo4j и DynamoDB, рекомендуют определенные принципы моделирования, адаптированные к их базовым механизмам. Хороший дизайн должен основываться на шаблонах доступа и конкретных внутренних механизмах базы данных. Правильное решение может повысить производительность на порядок, предотвращая необходимость в отчаянных, реактивных исправлениях в дальнейшем.

Моделирование данных подходит не только для NoSQL. Если вам интересно, YugaByteDB и TigerDB (обе основаны на PostgreSQL) также опубликовали свои собственные наборы рекомендаций. С ростом поддержки менее структурированных данных и повышением гибкости РСУБД проектирование схемы становится еще более актуальным.

Тем не менее, оптимизация иногда сопряжена со значительными накладными расходами. Следуя принципу Парето, в большинстве приложений 20% запросов составляют 80% рабочей нагрузки базы данных. Следует приоритизировать и оптимизировать именно эти запросы, а для остальных выбирать наиболее простую архитектуру — при условии, что они соответствуют базовым требованиям к производительности.

Переосмысление стратегий тестирования

Нам абсолютно необходимы автоматизированные тесты для уверенного рефакторинга запросов. Однако поддержание 100%-ного покрытия модульными тестами для взаимодействия с базой данных создает огромные накладные расходы. Часто это требует заполнения различных наборов данных для каждого возможного сценария, а чтение кода настройки тестов может быть сложнее, чем чтение самого запроса.

На мой взгляд, большинству простых CRUD-запросов не нужны изолированные модульные тесты. Их следует проверять во время разработки, но поддержание набора модульных тестов только для проверки работоспособности SELECT * — это плохая инвестиция. Любые непреднамеренные изменения в этих простых запросах следует выявлять во время проверки кода. Вместо этого полагайтесь на более высокоуровневые комплексные тесты — интеграционные, API- или сквозные (E2E) тесты — для обеспечения необходимой защиты.

Что следует тестировать модульно? Мы должны сосредоточиться на сложных запросах и крайних случаях, которые не очевидны для члена команды, читающего наш код. Большие, сложные запрос можно разбить на более мелкие логические части и протестировать их независимо:

  • MongoDB: Конвейеры агрегации естественным образом разбиваются на этапы. Это позволяет легко изолировать конкретные преобразования и тестировать их независимо, не запуская весь конвейер.
  • РСУБД: Массивные запросы часто становятся «черными ящиками». Рефакторинг их в общие табличные выражения (CTE) или представления позволяет нам декомпозировать их.

Пища для размышления: ограничения внешних ключей являются серьезным препятствием для модульного тестирования. Вы можете планировать тестировать запрос к одной таблице, но строгие ограничения внешних ключей могут потребовать заполнения данными нескольких других таблиц, не имеющих отношения к вашему тесту. Действительно ли нам нужны жесткие ограничения базы данных для всех связей, или мы можем позволить себе проверки на уровне приложения или асинхронные обновления для поддержания целостности данных?

Централизованное управление «невидимыми функциями»

Хранимые процедуры и триггеры часто «невидимы», потому что они не находятся в исходном коде приложения. Из-за этого они легко оказываются пропущенными, забытыми или сломанными.

Представьте себе такой сценарий: разработчик A развертывает триггер для обновления метки времени. Спустя несколько месяцев разработчик Б пишет миграцию схемы, которая непреднамеренно нарушает логику, реализованную разработчиком A. Это критическое изменение может оставаться незамеченным в течение нескольких недель, заставляя разработчиков просматривать журналы приложения в поисках «magical bug», не понимая, что проблема кроется в другом месте.

Я не являюсь категорическим противником хранимых процедур и триггеров. На мой взгляд, если они используются, то централизованное управление должно осуществляться кем-то. Специалист, обладающий опытом и хорошим пониманием работы базы данных, должен выполнять роль «проверяющего базы данных на вменяемость», чтобы следить за этими невидимыми функциями во время проверки кода.

Используйте проверку схемы базы данных

Под «проверкой схемы» я подразумеваю обеспечение соответствия как структуры данных, так и бизнес-логики. Проверки, выполняемые самой базой данных, действуют как централизованный контроль, критически важный для баз данных, к которым обращаются несколько сервисов, принадлежащих разным командам, когда несоответствие требований и графиков выпуска может легко привести к некорректным данным.

В дополнение к базовым структурным проверкам, базы данных SQL поддерживают ограничения CHECK CONSTRAINTS, которые позволяют нам применять регулярные выражения или диапазоны значений непосредственно к столбцам. Проверка схемы JSON в MongoDB позволяет применять аналогичные правила даже к полиморфным коллекциям (коллекциям, содержащим документы с различной структурой).

Использование ORM и ODM: палка о двух концах

Объектно-реляционные мапперы (ORM) и объектно-документные мапперы (ODM) добавляют уровень абстракции, позволяющий разработчикам писать запросы на языке, с которым они знакомы. Они также обеспечивают базовую очистку и валидацию.

Однако абстракции сопряжены с подводными камнями при неправильной настройке или неправильном понимании. Неожиданное поведение может привести к неожиданным результатам или снижению производительности (пример: проблема N+1). Очень часто их сложнее обнаруживать или исправлять, поскольку фактический запрос скрыт за вызовом метода, что требует более глубокого анализа точных операторов или команд, отправленных базовым драйвером.

Кроме того, эти абстрагированные мапперы редко хорошо справляются с выполнением сложных запросов или комплексных агрегаций. Ради согласованности кода мы можем быть вынуждены использовать идиоматический способ запроса маппера, запутывая код в непонятные узлы, чтобы получить работающий запрос. Часто проще писать их в виде простых запросов, и нам не нужно бояться обходить ORM или ODM.

Что дальше?

Технический долг, связанный с базами данных, не прощает ошибок. Универсального решения нет, но мы можем принимать осознанные решения, чтобы не усугублять ситуацию. Инвестируя в знания о базах данных и применяя прагматичные стратегии, мы можем держать этот долг под контролем. Независимо от того, используете ли вы реляционную базу данных или другую, цель состоит в том, чтобы иметь надежный, хорошо отлаженный механизм, адаптирующийся к изменениям, а не «долговую базу», к которой все боятся прикасаться.