Как создать миграции данных для самописной CMS.

Введение

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

Поиски решения

1. Как хранить номер текущей версии.

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

2. Структура версионной таблицы

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

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

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

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

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

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

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

3. Файлы миграций

Здесь есть несколько путей.

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

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

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

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

4. Хранение версии модели и списка миграций.

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

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

В голову так же приходит файл миграций где они хранятся в виде: название модели -> версия -> файл миграции.

Но в такой ситуации если название модели сменится, либо модель вовсе необходимо будет удалить, то становится сложнее находить миграции для нее.

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

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

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

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

Решение

Создать таблицу, в которой храним связки между названиями модуля и их версией.

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

При инициализации модуля, сравниваем версию модуля и актуальную.

Храним версию модуля в конфиге с информацией о нем. Там же храним список миграций в порядке их появления

Сравниваем актуальную версию с версией из конфига.

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

Вывод

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