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

Апгрейд существующих проектов
Если вы уже используете PHPixie то сделать апгрейд для использования миграций совсем просто.

1. Обновить конфиг соединения с БД (database.php)

PHPixie теперь поддерживает альтернативный синтаксис где вместо одной строки соединения используются дополнительные параметры, например:

Вместо старого:
return array(
    'default' => array(
        'connection' => 'mysql:dbname=phpixie',
        'user'     => 'phpixie',
        'password' => 'phpixie',
        'driver'   => 'pdo'
    )
);

теперь используется

return array(
    'default' => array(
        'database' => 'phpixie',
        'user'     => 'phpixie',
        'password' => 'phpixie',
        'adapter'  => 'mysql', // one of: mysql, pgsql, sqlite
        'driver'   => 'pdo'
    )
);

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

2. Скопировать папку /assets/migrate и конфиг файл /assets/config/migrate.php со скелета в свой проект.

Вот и все.

Конфигурация

Рассмотрим конфиг /assets/config/migrate.php:

<?php

return array(
    // настройки миграций
    'migrations' => array(
        'default' => array(
            
            // имя соединения с database.php
            'connection' => 'default',
            
            // путь в котором хранятся миграции, относительно папки /assets/migrate/
            'path'       => 'migrations',

            // не обязательно:
            
            // имя таблицы в которой хранить миграции
            'migrationTable' => '__migrate',

            // имя поля в таблице миграций
            'lastMigrationField' => 'lastMigration'
        )
    ),

    // настройки сидирования (об этом позже)
    'seeds' => array(
        'default' => array(
            
            // имя соединения с database.php
            'connection' => 'default',
            
            // путь в котором хранятся сиды, относительно папки /assets/migrate/
            'path' => 'seeds'
        )
    )
);

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

Создание и удаление базы

Создавать и удалять базу теперь можно прямо с консоли, это делает новая команда framework:database:

framework:database ACTION [ CONFIG ]
Create or drop a database

Arguments:
ACTION    Either 'create' or 'drop'
CONFIG    Migration configuration name, defaults to 'default'

То есть console framework:database create проверит существует ли база, и если нет то создаст ее, а console framework:database drop удалит ее.

Миграции


Ну а теперь о самом главном. Сначала короткое вступление для тех кто ничего подобного пока не использовал.

Миграции дают возможность хранить изменения структуры базы в коде что гораздо удобнее чем перебрасываться готовыми дампами а затем вручную изменять базу на продакшене. Принцип работы прост: в базе хранится имя последней миграции и при запуске команды будут применены все миграции которые «больше» ее в порядке natsort(), то есть если у нас есть файлы 1.sql, 2.sql… 22.sql, a последняя в базе 13.sql то выполнятся все от 14 до 22, и затем в базе сохранится 22 как имя последней. Они могут быть в формате .sql или .php.

SQL миграции


Тут все просто, это просто SQL файл в котором отдельные выражения разделяются сепаратором "-- statement", например:

CREATE TABLE fairies(
    id int NOT NULL,
    name VARCHAR(255)
);

-- statement

CREATE TABLE flowers(
    id int NOT NULL,
    name VARCHAR(255)
);

PHP миграции


Это просто PHP файл с возможностью выполнения запросов и даже доступа к запросам PHPixie Database:

$this->execute("CREATE TABLE fairies(
    id int NOT NULL,
    name VARCHAR(255)
)");

$this->message("Какое-то сообщение в консоль");

// привычные запросы
$this->connection()->updateQuery()
    ->table('users')
    ->set(['role' => 'user'])
    ->execute();

Кстати очень рекомендую в именах миграций писать краткое описание а не просто цифры. Поскольку используется порядок natsort то можно смело писать комент после знака _, например 33_fairies_table.sql

Тут сразу стоит ответить на 2 вопроса:

Почему нет down миграций для отката:

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

Почему изменения делаются сырыми SQL запросами а не универсальными методами типа createTable() ?

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

Сиды


Сиды — это данные которыми можно наполнить базу. Например это могут быть какие-то дефолтные пользователи, категории товаров итд, также их можно использовать для наполнения базы тестовыми данными для функциональных тестов. Имя файла должно совпадать с именем таблицы, доступные форматы .php и .json. Например:

// /assets/migrate/seeds/fairies.php

<?php

return array(
    array(
        'id'   => 1,
        'name' => 'Pixie'
    ),
    array(
        'id'   => 2,
        'name' => 'Trixie'
    ),
);

// /assets/migrate/seeds/flowers.json

[
    {
        "id": 1,
        "name": "daisy"
    },
    {
        "id": 2,
        "name": "Rose"
    },
]

В случае с .php кроме возвращения массива данных также есть возможность сделать все руками используя соединение с БД:

// /assets/migrate/seeds/fairies.php

<?php
$this->connection()->insertQuery()
    ->data([
        'id'   => 1,
        'name' => 'Pixie'
     ])
     ->execute();

Для вставки сидов используется команда framework:seed:

framework:seed [ --truncate ] [ CONFIG ]
Seed the database with data

Options:
truncate    Truncate the tables before inserting the data.

Arguments:
CONFIG    Seed configuration name, defaults to 'default'

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

Очевидно что для одного и того же соединения с БД можно задать несколько профилей сидов в файле конфигурации.

Использование без фреймворка


Как и все другие компоненты PHPixie Migrate можно использовать отдельно от фреймворка, примерно вот так:

$slice = new \PHPixie\Slice();
$database = new \PHPixie\Database($slice->arrayData(array(
    'default' => array(
        'database' => 'phpixie',
        'user'     => 'phpixie',
        'password' => 'phpixie',
        'adapter'  => 'mysql', // one of: mysql, pgsql, sqlite
        'driver'   => 'pdo'
    )
)));

$filesystem = new \PHPixie\Filesystem();
$migrate = new \PHPixie\Migrate(
    $filesystem->root(__DIR__.'/assets/migrate'),
    $database,
    $slice->arrayData(array(
    'migrations' => array(
        'default' => array(
            'connection' => 'default',
            'path'       => 'migrations',
        )
    ),
    'seeds' => array(
        'default' => array(
            'connection' => 'default',
            'path' => 'seeds'
        )
    )
)));

$cli = new \PHPixie\CLI();
$console = new \PHPixie\Console($slice, $cli, $migrate->consoleCommands());
$console->runCommand();

Здесь имена команд будут run, seed, database без префикса framework.

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

Комментарии (25)


  1. ekho
    15.11.2016 10:53
    +3

    Принцип работы прост: в базе хранится имя последней миграции и при запуске команды будут применены все миграции которые «больше» ее в порядке natsort(), то есть если у нас есть файлы 1.sql, 2.sql… 22.sql, a последняя в базе 13.sql то выполнятся все от 14 до 22, и затем в базе сохранится 22 как имя последней.


    При разработке с использованием веток будут проблемы. К примеру: создали ветку в ней миграцию 1. Чуть позже создали ветку с миграцией 2, которую смёржили в стабильную ветку и выкатили. Т.о. в базе записано, что последняя миграция это 2. Когда будет смёржена первая ветка миграция 1 не будет накачена т.к. она меньше 2.


    1. jigpuzzled
      15.11.2016 12:21
      +1

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


      1. ekho
        15.11.2016 13:09
        +2

        Никто ничего не меняет, в том то и дело. Миграции создаются разными разработчиками в параллельных ветках. Ветки имеют тенденцию мёржиться по мере готовности фичи (мы говорим про git flow или github flow). И совсем не иллюзорна ситуация, когда ветка с более новой миграцией попадёт в стабильную ветку раньше чем ветка с более старой миграцией.


        1. jigpuzzled
          15.11.2016 13:32
          +1

          Нет я понял. «Меняет» тот кто делает мердж. То есть ясно что перед тем как мерджить надо посмотреть все ли ок с миграциями. Кстати совсем не обязательно называть их 1, 2, 3 итд. Вот например Доктрина по дефолту генерирует названия типа Version20160527081937 используя дату и время. Так тоже будет работать конечно.


          1. jigpuzzled
            15.11.2016 14:07

            Здесь был чей-то комент, и я нечайно нажал «отменить». Сорри =(
            Закиньте коммент еще раз и я в этот раз нажму правильную кнопку

            Нашел в мейле:

            Так а если сделать просто проверку миграций что есть в базе и что есть на текущий момент в файлах?


            1. jigpuzzled
              15.11.2016 14:09

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


              1. vvasilenok
                15.11.2016 15:54

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


                1. jigpuzzled
                  15.11.2016 15:55

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


                  1. vvasilenok
                    15.11.2016 16:07

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


                    1. jigpuzzled
                      15.11.2016 16:11

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


                      1. vvasilenok
                        15.11.2016 16:52
                        +1

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


                      1. Vadiok
                        15.11.2016 20:03
                        +1

                        > надо думать перед тем как мерджыть код в мастер

                        А — накатываются все миграции, которых не было
                        Б — накатываются миграции, которые свежее последней

                        1. Человек не думает:
                        А — обычно ничего не произойдет, максимум, миграция не сработает, т.к. будут конфликт из-за другой миграции (поле уже есть, такая таблица уже создана и т.п.)
                        Б — код не работает, т.к. нет необходимых таблиц, полей и т.п.

                        2. Человек подумал:
                        А — запустил миграции, все ок
                        Б — переименовал миграции в особом порядке, запустил, все ок.

                        Итог — в случае Б вероятность все сломать, когда не думаешь выше, когда думаешь, надо выполнять лишние действия.


                        1. jigpuzzled
                          15.11.2016 20:04

                          Ну с апдейтом базы всегда есть возможность все сломать. А как вы апдейты делаете?


                          1. Vadiok
                            16.11.2016 09:52

                            > А как вы апдейты делаете?
                            1. Тестирование ветки на тестовом.
                            2. При успешном тестировании мержер мержит ветку задачи в мастер локально.
                            3. Выполняются миграции локально у мержера, если идут какие-то ошибки в локальных миграциях, то решается вопрос с разрабами или силами мержера.
                            4. Если локально у мержера все ок в мастере, то результаты мержа выкладываются в репу, выливаются на прод, запускаются миграции (скрипт миграций всегда запускается автоматом при пуле мастера на проде, дальше скрипт сам решает есть что выполнять или нет).


                            1. jigpuzzled
                              16.11.2016 16:03

                              Мммм ну я как раз так и советую. В чем проблема то?


                              1. Vadiok
                                16.11.2016 16:44

                                Проблема в том, что для шага 2 добавляются следующие шаги для мержера:
                                2.2. Смотрит, были ли файлы миграций в сливаемой ветке
                                2.3. Если были, то смотрит, какие у них даты и сравнивает с последним числом в базе.
                                2.4. Если последняя миграция свежее тех, что в ветке, то переименовывает файлы миграций из ветки, делает еще коммит в мастер с переименованием миграций.

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


                                1. jigpuzzled
                                  16.11.2016 17:41

                                  То есть ваша система миграций записывает все выполненые миграции, и если есть посередине пропущенные то все равно запускает и их поверх последней?


                                  1. Vadiok
                                    16.11.2016 17:52
                                    +1

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


                                  1. Zhuravljov
                                    16.11.2016 18:02

                                    А почему бы и нет?


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


                                    Коллизии возможны, но встречаются крайне редко, если разработчики в разных ветках обновляли одни и те же таблицы.


                                    Именно так миграции работают в Yii, и пока связанных с этим проблем при командной разработке не наблюдал.


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


                                    1. jigpuzzled
                                      16.11.2016 18:09

                                      vadiokZhuravljov

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

                                      1.sql: CREATE TABLE IF NOT EXISTS fairies(id int);
                                      2.sql: DROP TABLE IF EXISTS fairies;

                                      Очевидно что легко можно получить 2 разных состояния при том же списке миграций но в разном порядке. Пикся же всегда делала упор на строгость.


                                      1. Vadiok
                                        16.11.2016 18:21

                                        Тут вы сами себе проблему делаете из-за «IF (NOT) EXISTS» — там, где надо выводить ошибку, втихую пропускаете выполнение скрипта.
                                        Да и опять же, с какой стати такая ситуация возникнет, что в одной ветке таблицу только создают — это ок, в другой только ее удаляют — эта ветка уже будет зависеть от предыдущей.


                                      1. Zhuravljov
                                        16.11.2016 18:25

                                        Для строгости стоит выбросить IF [NOT] EXISTS. Тогда, вместо того, чтобы ничего не сделать и слиться по тихому, миграция отвалится с ошибкой. Это и будет сигналом о том, что что-то не так. Но, повторюсь, такое бывает крайне редко. В разных ветках под разные фичи, обновляются разные сущности.


                                        1. jigpuzzled
                                          16.11.2016 19:06

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


                                          1. Vadiok
                                            17.11.2016 09:54

                                            Я могу предположить 2 варианта, когда разработчики написали каждый свою миграцию, и последовательность этих миграций может вызвать ошибку в логике кода, не вызывая ошибки в базе:
                                            1. Назначение на 1 поле разных дефолтных значений.
                                            2. Расширение значений 1 и того же поля типа enum.
                                            + 3. Изменение типа поля — тут совпадение почти невероятно.

                                            2-й случай скорее всего вызовет ошибку независимо от последовательности миграций, т.к. 1 разработчику надо дописать 1 значение, другому — другое. В данном случае последовательность миграций только повлияет на то, у какого пользователя код неверно отработает (у того, чья миграция была выполнена раньеш).

                                            По поводу первого случая — тут да — надо следить, но это довольно редкий случай и следить довольно просто — просто смотреть есть ли установка дефолтного значения, если есть то глянуть, к чему может привести.

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


        1. Zhuravljov
          15.11.2016 16:09

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