Миграции структуры - перенос инфоблоков и настроек между стендами
Переносим правки структуры между стендами кодом: журнал миграций, поиск по кодам, повторный запуск и откат.
Механика
Структура проекта живёт в базе, а не в репозитории. Инфоблок, его свойства, пользовательские поля, почтовые шаблоны и настройки модулей - это обычные строки таблиц. Выложенный код их с собой не приносит, и новый шаблон на боевом сайте ищет свойство, которого там пока нет.
Обратный перенос базы задачу не решает, хотя выглядит проще всего. Дамп со стенда затирает заказы и правки контент-менеджеров за все дни разработки. Дамп с боевого стирает всё, что сделал разработчик. Базу поэтому возят в одну сторону: с боевого на стенд, и никогда обратно.
Миграция - это файл с кодом, приводящий структуру к нужному состоянию. У неё две стороны: применить изменение и отменить его. Файлы применяются по порядку имён, а отметки о применённых лежат в отдельном журнале.
Идентификаторы сущностей на стендах разные, и это ломает половину первых попыток. Инфоблок с номером двенадцать на боевом сайте на стенде окажется пятым, поэтому миграция ищет сущности по символьному коду и типу.
Повторяемость здесь важнее аккуратности кода и красоты имён в самой миграции. Журнал и реальность расходятся: кто-то поднял стенд из старой копии, кто-то накатил правку руками через интерфейс. Каждая миграция поэтому сначала смотрит на текущее состояние, а потом правит.
Откат существует далеко не всегда, и рассчитывать на него опасно. Удаление свойства уносит с собой все значения, поэтому на боевом сайте «отменить» чаще означает написать ещё одну миграцию вперёд.
Готового механизма миграций в самой платформе нет вовсе, и это придётся принять. Ближайшее штатное средство - шаги обновления своего модуля, но они привязаны к его версии. В командах берут готовый инструмент из Маркетплейса или пишут свой на полсотни строк.
Шаги
- Завести папку миграций и журнал применённых в отдельной таблице.
- Написать первую миграцию с применением и отменой, по общему шаблону.
- Искать инфоблоки, свойства и поля по символьному коду, а не по номеру.
- Прогнать миграции на стенде, а затем на свежей копии боевого сайта.
- Встроить прогон в выкладку между переносом кода и сбросом кэша.
- Договориться в команде о запрете: правки структуры руками на боевом сайте недопустимы.
Код
Заводим журнал применённых миграций:
class MigrationTable extends \Bitrix\Main\ORM\Data\DataManager{ public static function getTableName(): string { return 'vendor_migration'; }
public static function getMap(): array { return [ (new \Bitrix\Main\ORM\Fields\StringField('NAME'))->configurePrimary(true), new \Bitrix\Main\ORM\Fields\DatetimeField('APPLIED_AT'), // имя файла миграции и есть ключ: одна строка на одну применённую миграцию ]; }}Журнал - это ответ на вопрос «что уже применено на этом стенде». Имя файла в нём служит ключом, поэтому имена задают датой и коротким описанием и никогда не меняют после выкладки.
Находим инфоблок по символьному коду:
$iblock = \Bitrix\Iblock\IblockTable::getRow([ 'filter' => ['=CODE' => 'catalog', '=IBLOCK_TYPE_ID' => 'catalog'], 'select' => ['ID'],]);// номер инфоблока на стенде и на боевом разный, символьный код - общий// тип инфоблока в фильтре обязателен: коды в разных типах повторяютсяСимвольный код инфоблока и типа - это и есть переносимый ключ. Номер, взятый из адресной строки админки, работает ровно на том стенде, где его подсмотрели.
Добавляем свойство, если его ещё нет:
$exists = \Bitrix\Iblock\PropertyTable::getRow([ 'filter' => ['=IBLOCK_ID' => $iblock['ID'], '=CODE' => 'COUNTRY'], 'select' => ['ID'],]);if (!$exists) { (new CIBlockProperty)->Add(['IBLOCK_ID' => $iblock['ID'], 'CODE' => 'COUNTRY', 'NAME' => 'Страна', 'PROPERTY_TYPE' => 'S', 'ACTIVE' => 'Y']);}// проверка перед правкой и есть та самая повторяемость// точно так же проверяют группы свойств, разделы и значения списковПроверка существования занимает три строки и снимает целый класс поломок. Миграция после неё переживает повторный запуск, ручную правку и стенд, поднятый из копии месячной давности.
Описываем отмену рядом с применением:
public function down(): void{ $prop = \Bitrix\Iblock\PropertyTable::getRow([ 'filter' => ['=IBLOCK_ID' => $this->iblockId, '=CODE' => 'COUNTRY'], 'select' => ['ID'], ]); if ($prop) { CIBlockProperty::Delete($prop['ID']); // значения свойства уйдут вместе с ним // на боевом сайте такую отмену делают только с дампом под рукой }}Отмена нужна прежде всего на стенде, где ветки переключают по десять раз в день. На боевом сайте её применяют осознанно и редко, понимая, какие данные пропадут вместе со структурой.
Переносим настройку модуля и своё поле:
\Bitrix\Main\Config\Option::set('vendor.shop', 'order_source', 'site');$uf = new CUserTypeEntity();$uf->Add(['ENTITY_ID' => 'USER', 'FIELD_NAME' => 'UF_DEALER', 'USER_TYPE_ID' => 'boolean', 'EDIT_FORM_LABEL' => ['ru' => 'Дилер']]);// существование поля проверяют тем же способом, что и свойство инфоблокаМиграцией переносят не только инфоблоки. Настройки модулей, пользовательские поля, почтовые события и шаблоны писем ведут себя одинаково: в интерфейсе они задаются за минуту и теряются при первом же переносе базы.
Прогоняем непринятые миграции:
foreach (glob(__DIR__ . '/../migrations/*.php') as $file) { $name = basename($file, '.php'); if (MigrationTable::getByPrimary($name)->fetch()) { continue; // эта уже применена на стенде } (require $file)->up(); MigrationTable::add(['NAME' => $name, 'APPLIED_AT' => new \Bitrix\Main\Type\DateTime()]); echo "применена {$name}\n"; // вывод нужен и в консоли, и в журнале выкладки}Порядок задают именами файлов, а не датой правки на диске. Отметку ставят сразу после успешного применения, иначе упавшая посередине миграция при следующем запуске начнётся заново.
Запускаем прогон из консоли:
// /local/tools/migrate.php - запускается руками и из выкладки$_SERVER['DOCUMENT_ROOT'] = realpath(__DIR__ . '/../..');define('NO_KEEP_STATISTIC', true);define('NOT_CHECK_PERMISSIONS', true);require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';// в консоли нет ни текущего сайта, ни пользователя: всё нужное задают явноКонсольный запуск важнее веб-страницы с кнопкой. Миграции идут минутами, а браузер обрывает такой запрос по своему таймауту в самый неудачный момент.
Сбрасываем кэш после правки структуры:
$GLOBALS['CACHE_MANAGER']->ClearByTag('iblock_id_' . $iblock['ID']);BXClearCache(true, '/catalog/');// после новых свойств фасетный индекс пересобирают отдельноНовое свойство появляется в базе мгновенно, а на витрине - после сброса кэша. Это стоит делать той же командой, что и прогон, иначе половина выкладок заканчивается вопросом «почему не видно».
Ставим прогон в порядок выкладки:
git pull --ff-onlyphp local/tools/migrate.php up # структура догоняет уже выложенный кодphp local/tools/cache-clear.php # кэш сбрасывают последним, уже по новой структуреМесто прогона в списке шагов важно. Код приезжает первым, структура догоняет его вторым шагом, кэш сбрасывают последним; при обратном порядке сайт успевает собрать страницы по старой структуре.
Ограничения
Штатного механизма миграций в платформе нет, и это стоит признать сразу. Написанное выше - либо свой прогон на полсотни строк, либо готовый инструмент; и то, и другое требует договорённости команды, а не только кода.
Миграции переносят структуру проекта, но не переносят его контент и данные. Товары, страницы и пользователи живут только на боевом сайте, а на стенд приезжают дампом, и путать эти два потока нельзя ни в одну сторону.
Тяжёлые миграции задерживают выкладку на время, которое заранее никто посчитать не берётся. Правка миллиона элементов на боевой базе выносится в фоновое задание с порциями, а сама миграция только запускает его и отмечается в журнале.
Две ветки с миграциями рано или поздно встречаются на одном боевом сайте. Порядок применения тогда решают имена файлов, поэтому в имя ставят дату, а не сквозной номер: номера в двух ветках совпадут обязательно.
Готовые инструменты миграций из Маркетплейса закрывают эту задачу целиком, и на командном проекте выбирают обычно их. Свой прогон оправдан там, где правок структуры немного, а лишний сторонний модуль в проекте не нужен.
Правка структуры руками на боевом сайте не запрещается технически. Её убирают договорённостью и правами: у контент-менеджера не должно быть доступа к настройкам инфоблоков, иначе журнал миграций перестаёт что-либо значить.
Типичные проблемы
После выкладки шаблон не находит свойство.
Свойство завели руками только на стенде, через интерфейс админки, и в код это не попало. Структуру переносят миграцией в том же порядке, что и код самого проекта.
Дамп с тестового затёр заказы и правки менеджеров.
Базу перенесли целиком вместо того, чтобы перенести миграцией одну структуру. Дамп едет только с боевого сайта на стенд разработчика, но никогда в обратную сторону.
Миграция прошла на стенде и упала на боевом.
В коде записан номер инфоблока, а на стендах он разный. Сущности в миграции ищут по символьному коду и типу, а не по номеру.
Повторный запуск создал второе такое же свойство.
Миграция правит структуру вслепую, без проверки того, что уже сделано раньше. Перед любой правкой обязательна проверка того, что сущности на стенде ещё нет.
Откат миграции удалил свойство вместе со значениями.
Отмена на боевом сайте выполняет удаление, а не возврат данных. На боевом сайте вместо отмены чаще пишут ещё одну миграцию вперёд.
Новое свойство есть в базе, но не видно на витрине.
Кэш компонентов и фасетный индекс остались со старой структурой. Кэш и фасетный индекс сбрасывают тем же шагом выкладки, что и сам прогон миграций.
Частые вопросы
Как залить дамп базы с тестового на боевой без потери данных?
Никак: на боевом сайте за это время появились заказы и правки контент-менеджеров. Между стендами переносят структуру миграциями, а базу возят только в обратную сторону.
Нужны ли миграции, если разработчик один?
Да, как только стендов становится больше одного. Через полгода никто не вспомнит, какие правки структуры уже уехали на боевой, а какие остались на стенде.
Чем делать миграции в Битриксе?
Готовым инструментом из Маркетплейса или своим прогоном на полсотни строк. Штатное близкое средство - шаги обновления своего модуля, но они привязаны к его версии.
Как быть с разными номерами инфоблоков на стендах?
Искать инфоблок по символьному коду и типу, а номер получать уже из выборки. Номер из адресной строки админки годится только для того стенда, где его подсмотрели.
Что делать с правками структуры, сделанными руками на боевом?
Повторить их миграцией и отметить применённой, чтобы стенды пришли к тому же состоянию. Дальше - закрыть доступ к настройкам инфоблоков тем, кому он не нужен.
Смежное
-
Git и выкладка - оглавление подтемы
-
После выкладки сайт сломался: разбор причин - главная причина поломок после выкладки
-
Выкладка на боевой: порядок, структура, откат - куда встраивается прогон миграций
-
Три контура проекта: разработка, тест, бой и порядок переноса - зачем миграции нужны трём контурам
-
Git в проекте на платформе: состав репозитория и выкладка - что вообще лежит в репозитории
-
Стенд из копии боевого: подъём, обезличивание, отрезанные связи - откуда на стенде берётся актуальная база
-
Своя сущность от таблицы до админки - структура своей таблицы и её изменения
-
Переезд с самописной таблицы на инфоблоки - перенос данных, а не одной структуры
-
Проверки перед выкладкой: синтаксис, стандарт, тесты, миграции - прогон миграций на копии базы
-
Архитектура проекта - что где лежит в проекте на платформе
-
Highload-блок на объёме: индексы, заливка, выкладка структуры - перенос структуры справочника
-
Перенос универсального списка между стендами: структура, права, данные - миграция списка и его прав
-
Перенос инфоблока через XML: структура и данные - штатная выгрузка вместо миграции кодом