[Из песочницы] Антипаттерны работы с базами данных
Привет, Хабр! Представляю вашему вниманию перевод статьи «Database: Anti-Patterns» автора Sergey Kozlov.
Если вы храните данные, это критически важная часть вашего приложения. Можно легко и быстро задеплоить исправление бага на новом сайте знакомств, чтобы фермер Джо из северного Техаса наконец-то смог прочитать последнее сообщение от любовницы по переписке и узнать, что ей нравятся лысые мужчины. Но упаси вас бог потерять или испортить пользовательские данные.
Кремниевая долина, 2 сезон, 8 серия.
Тем не менее, многие разработчики не вполне понимают эту простую истину. Я не так уж много лет профессионально программирую, но уже повидал много, очень много ошибок, которые совершают люди, работая с БД.
Вот лишь те, что сразу приходят в голову.
Отсутствие бэкапов
«Делайте бэкапы» — одно из таких правил (вроде «не работай под рутом» или «пристегивайте ремни»), с которыми многие из нас согласны, но не следуют им, надеясь, что плохие вещи случаются с другими, а не с нами.
Кстати, если вы не тестируете восстановление из бэкапов, можете считать, что бэкапов у вас нет вообще. Учитесь на чужих ошибках:
Другими словами, из пяти задеплоенных техник резервного копирования / репликации ни одна не работает надежно или не настроена. В конце концов, мы восстановили данные из бэкапа, сделанного 6 часов назад.
Мы потеряли данные базы за 6 часов (issues, merge requests, пользователи, комментарии, сниппеты и др.) с GitLab.com.
NoSQL
Бывает, что у ваших пользователей слишком много контента для взрослых и они слишком часто его смотрят объем данных слишком велик или нагрузка слишком высока, чтобы реляционная БД могла справиться. Это тот случай, когда в игру вступают NoSQL-технологии. Софтверные гиганты вроде Google знакомы с этим не понаслышке.
Но вы не Google. Несколько сотен гигабайт — это не «big data», а 1000 комментариев в день — не «high load». Скорее всего, для ваших данных вполне достаточно PostgreSQL. Смотрите: он даже поддерживает JSON и умеет его индексировать.
Бросьте, вы серьезно хотите пожертвовать надежной структурой ради фич, которые вам не нужны и — посмотрим правде в глаза — никогда не будут нужны? Вы не станете новым Гуглом — у вас просто будет бардак в базе.
Слишком свободная схема
Это более актуально для NoSQL, но пользователи реляционных СУБД зачастую забывают или ленятся создавать все нужные ограничения. Из-за ошибки в коде приложения NULL
может быть сохранен там, где ожидается осмысленное значение, или может быть создана ссылка на отсутствующую запись. Впоследствии вы это замечаете и исправляете код, но понятия не имеете, как исправить данные.
Естественные первичные ключи
Представим, что мы хотим хранить пользователей, у каждого из которых должен быть уникальный e-mail. Самое очевидное решение — создать таблицу user
с колонкой email
, которая будет еще и первичным ключом.
К сожалению, естественный ключ может стать неприемлемым в качестве первичного при изменении требований (а они меняются постоянно). Сегодня PRIMARY KEY(email)
работает, а завтра мы решаем добавить регистрацию через Facebook и сделать e-mail необязательным. Что лучше: генерировать уникальные адреса и добавить флаг, обозначающий фиктивный e-mail, или изменить первичный ключ, все внешние ключи, которые ссылаются на user
, и т.д., и т.п.? Нам не пришлось бы выбирать меньшее из зол, если бы мы просто использовали суррогатный первичный ключ.
Логика в хранимках
Мне это не нравится по двум причинам:
- Код приложения обычно намного проще обновить, чем схему БД.
- Все эти PL SQL-ли напоминают мне Паскаль, и они такие же уродливые.
Миграционные скрипты, специфичные для конкретных сред
Я знаю, что иногда выбора нет, но в целом лучше стараться, чтобы все среды (dev, test, prod и т.д.) были как можно более похожими. Чем больше разница между средами, тем больше шансов допустить ошибку и обнаружить это только на проде.
Обычно даже DML-скрипты могут быть универсальными. Разные схемы же чаще всего — чистое зло.
Поэтому, когда я вижу в Liquibase-скриптах метки (labels), специфичные для конкретных сред, мне хочется убивать.
Толерантные миграционные скрипты
IF NOT EXISTS
и подобные вещи в DDL не нужны, если во всех средах одинаковые схемы, но могут маскировать ошибки. Если во время обновления БД случается что-то неожиданное, я предпочитаю узнать об этом и исправить как можно скорее, а не ломать голову неделю спустя, как исправить бардак.
Неатомарные обновления
Допустим, вы запустили changeset на продакшн-базе, и миграция не прошла успешно. Вы что-нибудь исправляете и хотите попробовать снова. Получится ли? Что если одни операции changeset-а закоммитились, а другие нет?
Вы можете заметить, что на самом деле это история о том, что changeset-ы должны быть идемпотентными, и будете правы.
К сожалению, многие разработчики, думая об идемпотентности, используют IF NOT EXISTS
или типа того. В предыдущем разделе я объяснил, почему это зло.
Вместо этого делайте changeset-ы атомарными. Тогда в случае ошибки совершенные изменения будут откачены и у вас не будет проблем с последующим применением этого changeset-а.
Но будьте внимательны, рассчитывая на транзакции. Например, поддержка DDL-выражений в транзакциях MySQL темна и полна ужасов, так что я всегда создаю отдельный changeset для каждого DDL-выражения, когда пишу Liquibase-скрипты для MySQL.
А какие антипаттерны видели вы?