За редким исключением, приложения и базы данных должны быть развернуты для использования. Будь то установка/обновление вашего приложения для нескольких клиентов или развертывание ваших изменений в средах Dev/UAT/Prod, вы должны иметь возможность продвигать свой код детерминированным способом, не требующим ручного вмешательства.

Вы не можете/не должны стоять за плечом вашего клиента, пока он устанавливает приложение, и объяснять ему «теперь измените эту таблицу и выполните эти 3 скрипта» или «Вот, восстановите это резервная копия». Ожидание сделать это таким образом непрофессионально, как правило, даже неосуществимо и всегда чревато человеческими ошибками.

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

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

Каждому разработчику нужен свой экземпляр

Это может показаться странным, расточительным или даже лишним, но каждый разработчик должен работать со своей собственной базой данных. Обычно находят способ запустить его на локальном хосте (для этого хорошо подходят контейнерные экземпляры сервера базы данных через что-то вроде Docker), но наличие центрального сервера разработки, на котором работает отдельная база данных для каждого разработчика, вполне допустимо.

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

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

Каждое изменение базы данных должно быть кодифицировано

Когда необходимо внести изменения в базу данных. Важно, чтобы изменение было кодифицировано. Таким образом, изменение можно воспроизвести для разных разработчиков и сред. Выяснение того, какие изменения необходимо внести, и их выполнение осуществляется с помощью инструментов. Вот несколько примеров из них: Entity Framework, Liquibase и FlyWay.

Внесение необходимых изменений часто называют «миграциями» или «эволюциями».

Когда я добавляю новую функцию, которую нужно изменить, я не отправляю электронное письмо с сообщением «Эй, всем добавить новый столбец в таблицу «Пользователи»!». Вместо этого команда ALTER TABLE ... записывается в сценарий любого инструмента, используемого для переноса изменений. Хотя я, вероятно, сообщу команде, какие изменения я вношу и почему, фактические изменения не организуются человеком.

Кодирование изменений и использование какого-либо метода миграции позволяет перенести изменения, которые я вношу в свою базу данных, на других разработчиков. Это также означает, что я могу выполнить развертывание в среде UAT/Testing, а затем, после утверждения, я могу быть уверен, что при развертывании в рабочей среде будут внесены точно такие же изменения.

Кодифицированные изменения должны быть неизменными

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

Вместо этого изменение необходимо добавить как еще одну миграцию.

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

Закодированные изменения могут быть проверены на проверку кода

Поскольку изменение базы данных записывается в коде, а не какой-то случайный разработчик или СУБД, вручную выполняющие сценарии в различных средах, они могут/должны быть рассмотрены в ваших запросах на включение. Таким образом, изменения легче сообщаются, а ошибки выявляются быстрее.

Часто вносите кодифицированные изменения

Одновременное внесение нескольких изменений в базу данных от нескольких разработчиков может стать огромной проблемой. Вместо этого изменения следует часто объединять в централизованную общую ветвь системы управления версиями. Разработчикам следует часто объединять свои изменения (надеюсь, с помощью запросов на извлечение) и часто следует разветвлять последний код, чтобы вносить будущие изменения. Таким образом, конфликты или ошибки обнаруживаются раньше, а масштаб этих ошибок/конфликтов сводится к минимуму.

Интеграция часто приводит к…

Используйте автоматизацию для развертывания миграций

Именно здесь вступает в игру CI/CD. Когда код интегрируется в общую ветвь кода, необходимо выполнить автоматическую сборку для проверки и тестирования с последующим немедленным развертыванием изменений в центральной среде интеграции. Это гарантирует, что шаги развертывания могут быть выполнены от начала до конца, приложение может быть развернуто, а миграция может быть запущена.

Повторим еще раз: развертывание вашей базы данных, будь то первоначальная установка или миграция для ее обновления, должно быть автоматизировано. Это должно быть сделано последовательно во всех средах точным и логичным образом. Это можно сделать с помощью какого-либо конвейера выпуска, такого как Azure DevOps, TeamCity, GitHub Actions и т. д.

Автоматизируя процесс развертывания и миграции, вы систематизируете предпринятые шаги, вводите секреты и снижаете вероятность того, что в уравнение будет внесена человеческая ошибка.

Дополнительные варианты использования

Общая база данных

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

Исходные данные

Базы данных по-прежнему можно заполнить с помощью скриптов. Эти начальные сценарии не должны запускаться в рабочей среде. Запускаются ли они отдельно или как-то пропускаются в определенных средах.

Лучше, если они каким-то образом могут быть указаны для работы в одних средах, но не в других. Причина этого в том, что если вы записываете свои начальные данные, они могут продолжать «мигрировать» после того, как они были записаны. Напротив, если вы пишете исходные сценарии отдельно и хотите, чтобы ваши разработчики запускали их вручную, вам необходимо поддерживать их. Например, если ваш исходный скрипт вставляет 10 (еще хуже, 100 или 1000) строк в определенную таблицу, а позже вы удаляете столбец, вам придется изменить исходный сценарий и удалить этот столбец из всех этих операторов INSERT. Если бы вместо этого вы могли условно выполнить сценарий как часть процесса миграции, то удаление столбца произошло бы после того, как данные были вставлены, и сценарий начального значения не нужно было бы исправлять.

Один из примеров этого можно реализовать в Liquibase, который позволяет указать «контекст», в котором миграция или «changeSet» будут выполняться только в определенных контекстах (хотя использовать это в Liquibase, вы хотите всегда предоставлять контекст для команды обновления, потому что, если он пуст, ВСЕ контексты запускаются). Следовательно, начальная миграция в Liquibase может быть указана с таким контекстом, чтобы запускаться только в dev, а не в какой-либо другой среде (также возможны более сложные выражения, такие как !prod and !qa):

<changeSet id="Insert test data to table XYZ" context="dev">
    INSERT INTO ...
    ...
</changeSet>

Выполнение миграции в dev предоставит контекст для команды обновления:

liquibase --changeLogFile=changelog.xml --contexts="dev" update

И если бы вы выполнялись в prod, вы бы предоставили этот контекст, и он не запускал бы начальный набор изменений:

liquibase --changeLogFile=changelog.xml --contexts="prod" update

Заключение

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

Приложения меняются.

Базы данных меняются.

То, как вы внедряете эти изменения, оказывает значительное влияние на состояние вашего проекта и вашу компетентность в глазах вашего клиента и команды.