Еще немного о миграциях. Версия для PHP

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

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

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

Начав работать с Ruby on Rails довольно быстро знакомишься с механизмом миграций, а уже через некоторое время не понимаешь, как можно вообще было обходиться без этого невероятно удобного инструмента.
Придя в проект, который разрабатывался на PHP, я постарался привнести в него хотя бы минимальный набор полезных инструментов, знакомых по опыту общения с Ruby on Rails. Одним из пунктов была система для поддержки миграций.
После некоторого поиска выбор был сделан в пользу Ruckusing Migrations, как наиболее похожего на то, что я видел в рельсах. Рапортую, что по прошествии более полугода полет нормальный!

Установка


Предполагается, что Вы используете Composer для управления зависимостями. Если нет, обязательно попробуйте, оно того стоит!
Также при желании Вы можете клонировать с GitHub репозиторий с примером: github.com/ArtemPyanykh/php_migrations_example

Для начала добавьте в свой composer.json:
"require" : {
    "ruckusing/ruckusing-migrations": "dev-master"
}

и выполните
~/dev/php_migrations_example(master)$ php composer.phar install

Composer подтянет Ruckusing Migrations и установит в директорию vendors.

Далее, я советую поступить следующим образом:

  • Во-первых, создайте следующую структуру директорий:
    .
    └── db
        ├── logs
        ├── migrations
        │   ├── main
        │   ├── php_migrations_example -> main/
        │   └── php_migrations_example_test -> main/
        └── utility
    

    В директории db будут храниться Ваши миграции, конфиги и другие полезные штуки. Считайте этот каталог точкой отсчета для всего, что связано с миграциями.
    Сами файлы с миграциями будут храниться в db/migrations/main.
    php_migrations_example и php_migrations_example_test — это названия девелоперской и тестовой баз данных.
    Каталоги с соответствующими именами это просто символические ссылки на директорию main, так как вряд ли у Вас будут разные миграции для разных окружений.

  • Создайте файл db/ruckus со следующим содержимым:
    #!/bin/bash
    ruckus_dir="./../vendor/ruckusing/ruckusing-migrations"
    if [ ! -d $ruckus_dir ]; then
        echo "Ruckusing-Migrations wasn't detected. Please, use Composer to install the library."
        exit
    fi
    if [ ! -f "ruckusing.conf.php" ]; then
        echo "Ruckusing conf. file wasn't detected. Please, create proper ruckusing.conf.php."
        exit
    fi
    if [ "$#" -lt 1 ]; then
    	echo "At least 1 argument required"
    	echo "See ./ruckus --help"
    	exit
    fi
    if [ "$1" == "--help" -o "$1" == "-h" ]; then
    	echo "Usage: ./ruckus [--help] <task-name> [<parameter1>[ <parameter2> [...]]]"
    	echo ""
    	echo "The available ruckus commands are:"
    	echo "    ./ruckus db:migrate                     Ruckus scenario for tasks such as db:migrate, db:setup, etc."
    	echo "    ./ruckus db:generate <migration_name>   Ruckus scenarion for generating migration scaffolding"
    	echo ""
    	exit
    fi
    php $ruckus_dir"/ruckus.php" "$1" "${@:2}"
    

    Это просто небольшой скрипт, который я позволил себе написать, дабы упростить работу с Ruckusing Migrations.

  • Наконец, создайте конфиг-файл db/ruckusing.conf.php:
    <?php
    //----------------------------
    // DATABASE CONFIGURATION
    //----------------------------
    /*
    Valid types (adapters) are Postgres & MySQL:
    'type' must be one of: 'pgsql' or 'mysql'
    */
    return array(
        'db' => array(
            'development' => array(
                'type'      => 'mysql',
                'host'      => 'localhost',
                'port'      => 3306,
                'database'  => 'php_migrations_example',
                'user'      => 'root',
                'password'  => 'root'
            ),
            'test'  => array(
                'type'  => 'mysql',
                'host'  => 'localhost',
                'port'  => 3306,
                'database'  => 'php_migrations_example_test',
                'user'  => 'root',
                'password'  => 'root'
            )
        ),
        'migrations_dir' => RUCKUSING_WORKING_BASE . '/migrations',
        'db_dir' => RUCKUSING_WORKING_BASE . '/utility',
        'log_dir' => RUCKUSING_WORKING_BASE . '/logs'
    );
    ?>
    


Все, больше никаких настроек не требуется, Вы успешно интегрировали себе систему миграций!

Использование


В целом все очень просто. Давайте начнем с того, что сгенерируем миграцию:
~/dev/php_migrations_example(master)$ cd db
~/dev/php_migrations_example/db(master)$ ./ruckus db:generate CreateTestTable
	Created migration: 20130508145210_CreateTestTable.php

Вы можете заметить, что в каталоге db/migrations/main после этого добавится файл примерно с таким же названием (timestamp будет другой) следующего содержания:
<?php

class CreateTestTable extends Ruckusing_Migration_Base
{
    public function up()
    {
    }//up()

    public function down()
    {
    }//down()
}

Миграции обладают тем свойством, что могут быть не только применены, но и отменены, если существует адекватный способ отката изменений. Именно такова семантика методов up() (применение изменений) и down() (откат изменений). Давайте создадим таблицу test с несколькими полями и парой индексов. Дополним файл следующим образом:
<?php

class CreateTestTable extends Ruckusing_Migration_Base
{
    public function up()
    {
        $table = $this->create_table('test', array('options' => 'ENGINE=InnoDB DEFAULT CHARSET=utf8'));

        $table->column('this', 'integer', array('unsigned' => true, 'null' => false, 'default' => '42'));
        $table->column('that', 'string', array('limit' => '7'));
        $table->column('those', 'datetime');
        $table->finish();

        $this->add_index('test', array('this', 'that'), array('unique' => true));
    }//up()

    public function down()
    {
        $this->drop_table('test');
    }//down()
}

и запустим миграции:
~/dev/php_migrations_example/db(master)$ ./ruckus db:migrate
Started: 2013-05-08 7:05pm MSK
[db:migrate]: 
	Schema version table does not exist. Auto-creating.
	Creating schema version table: schema_migrations
	Migrating UP:
========= CreateTestTable ======== (0.31)
Finished: 2013-05-08 7:05pm MSK

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

Также просто можно откатиться или запустить миграции для другого окружения:
~/dev/php_migrations_example/db(master)$ ./ruckus db:migrate VERSION=-1
Started: 2013-05-08 7:13pm MSK
[db:migrate]: 
	Migrating DOWN:
========= CreateTestTable ======== (0.21)
Finished: 2013-05-08 7:13pm MSK
~/dev/php_migrations_example/db(master)$ ./ruckus db:migrate ENV=test
Started: 2013-05-08 7:14pm MSK
[db:migrate]: 
	Schema version table does not exist. Auto-creating.
	Creating schema version table: schema_migrations
	Migrating UP:
========= CreateTestTable ======== (0.24)
Finished: 2013-05-08 7:14pm MSK

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

Замечания


  1. Если вы попробуете запустить скрипт ruckus не из директории db/, то получите ошибку. Это связано с тем, что все пути в скрипте относительные, и при желании это легко исправляется. Нужно, однако, учесть один момент: по умолчанию, конфиг ищется в рабочей директории.
  2. При применении миграций на продакшене нужно быть очень осторожным: если у Вас есть достаточно увесистая таблица, скажем в несколько гигабайт, и Вы примените к ней миграцию, которая каким-либо образом меняет схему, скорее всего будет беда. Хотя это, возможно, и не недостаток миграций как таковых, а скорее недостаток СУБД, тем не менее это несколько ограничивает возможности применения системы. Для обновления больших таблиц нужно использовать специализированные инструменты, например Percona Toolkit.


Ссылки


Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 21

    +1
    Я представлял себе решение, которое сравнивает production и development БД, и создаёт, полностью самостоятельно, соответствующие миграции (SQL-файлы) с данными.

    В вашем случае — разработчику также достаётся ручное отслеживание, что именно изменилось и какие изменения нужно внести в production БД. Согласен, что это намного удобнее, чем ручной цикл (особенно в плане «версионного контроля» БД), но для полной автоматизации процесса ещё есть место. :)
      0
      Автоматическая генерация с возможностью вставлять «подсказки» вручную (правда, для постгреса и без генерации откатов) — вот: github.com/DmitryKoterov/dklab_pgmigrator (сделана на основе замечательного apgdiff-а Мирослава Шульца).
        +1
        Не совсем понятно, что имеется в виду под «ручным отслеживанием». Если нужно что-то изменить — вместо того, чтобы ручками править БД, напишите миграцию. Если придерживаться этого подхода и не править БД руками!, то все, что нужно будет сделать любому разработчику для приведения продакшена в актуально состояние, это набрать в консоли
        ./ruckus db:migrate
        (за исключением случая очень больших таблиц)

        Касательно инструмента, который бы сравнивал схему и самостоятельно генерировал миграционные SQL-файлы, тут достаточно сложный вопрос. Я когда-то использовал Toad for MySQL (если мне не изменяет память), но результаты были иногда просто обескураживающие (в плохом смысле).
          0
          вместо того, чтобы ручками править БД, напишите миграцию

          Вот если взять пример из статьи, то написать соответствующий SQL-запрос для изменения базы получится и быстрее и нагляднее, чем «миграцию»
            0
            А теперь представьте (немного утрировано), что схема у Вас меняется несколько раз в день, в течение хотя бы полугода, и в команде хотя бы 2 человека, причем каждый может менять схему. Я думаю уже через пару недель у Вас начнутся проблемы, при использовании быстрого и наглядного подхода с SQL-запросами :) Не устанете рассказывать сокаманднику Васе, что и как Вы сегодня поменяли в базе?

            На самом деле, кажется что долго с миграциями из-за большого сетапа (относительно). Через некоторое время написание миграции занимает в разы меньше времени, чем использование голого SQL.

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

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

            Например:
            Разработчик закончил пились свою фичу, отполировал код, и надо ему написать миграцию для БД. Он запускает скрипт, где указывает целевую БД и свою БД как источник. Скрипт сравнивает базы и генерирует два SQL-файла: миграцию и откат (что по сути является обратной миграцией с целевой БД на БД разработчика).
            Остаётся только заполнить соответствующие файлы/методы/функции в коде миграции — и вуаля. :)

            DmitryKoterov, спасибо за ссылку, гляну.
            0
            Я когда на работу устраивался — это было моим тестовым заданием — написать скрипт, который сравнит 2 mysql базы и выдаст sql файл, которые приведёт одну базу к состоянию другой. (структура + опционально данные)
            В общем примерно за час я накидал более-менее работоспособный прототип, дальше делать не стал — так взяли :)) при желании за вечер можно написать полностью :)
              +1
              Не уверен, что тут все так просто.
              Если данные не интересуют, то это можно просто дропнуть исходную БД, а потом создать заново из дампа целевой. Если все же хотелось бы сохранить какие-то данные, причем не только те, что находятся в «пересечении» атрибутов таблиц исходной и целевой БД, я прямо так сразу и не могу предложить адекватного решения. Например, как отработать такую ситуацию, без какой-либо дополнительной информации?
              Была следующая схема у таблицы test, например:
              a -> int
              b -> string
              c -> string
              

              Стала
              a -> int
              d -> string
              e -> string
              

              Где c была переименована в d, а b была переименована в e.
                0
                Эм… вы поставили меня в тупик :) Задача была несколько другая… видимо я что-то неправильно понял. Задача заключалась в выкладывании dev базы на продакшен… для этого нужны было найти отличия dev версии и сгенерировать файл, приводящий продакшн к состоянию dev версии.
                В данном случае скрипт бы дропнул b и c и создал бы новые поля d и е.
                Полноценный анализ, конечно, на коленке не напишешь… да и без истории изменений или человеческого вмешательства не обойтись…
                Что касается конкретно того скрипта, который я писал — он создавал массивы со структурами обеих баз, сравнивал — записывал результаты сравнения в другой массив (какие таблицы и поля добавились, какие изменили свой тип или другие атрибуты, какие удалились) и на его основе генерировал sql запросы. Аналогично потом пробегал по самим данным.
                Было не сложно, ибо всегда одна база была ведущая, а другая отстающая — надо было отстающую обновить до состояния ведущей, а не делать общую базу на основе обоих.
                0
                Поясните алгоритм, очень интересно.
                Присоединяюсь к предыдущему комментарию Deshene.
            • UFO just landed and posted this here
                0
                Да, Вы правы, делает то же самое. Но, несмотря на то, что Symfony — замечательный фреймворк, свет на нем клином не сошелся, а с «голой» Doctrine, по-моему не так удобно работать в плане миграций.
                Вообще сейчас большая часть современных фреймворков реализует механизм миграций в том или ином виде, и если начинать новый проект, лучше использовать готовую инфраструктуру. Описанное в статье решение — это, на мой взгляд, больше для легаси.
              0
              Чтобы оно действительно напоминало рельсы, было бы неплохо иметь метод change вместо up и down
              Писать постоянно миграции в обе стороны крайне напряжно. Особенно с учетом того, что очень мала веротяность что миграции вниз реально понадоятся.
                0
                Именно для этого нам и даны Pull Request'ы :)
                Хотя реализация change может быть несколько затруднительна: все-таки PHP не настолько крут в метапрограммировании как Ruby.
                  0
                  В Yii можно глянуть реализацию behaviors и в зависимости от направления подключать разные классы с реализацией методов типа create_table. В rails миграциях почти нет никакой мета-магии.

                  Кстати миграции есть и в самом Yii, только ещё не видел людей, которые их там использовали.
                    0
                    Кстати миграции есть и в самом Yii, только ещё не видел людей, которые их там использовали.

                    Приятно познакомиться :)
                0
                Миграции, конечно, удобны когда работаешь в команде, но если проект делаешь сам — только лишняя трата времени на их создание.
                  0
                  Я бы с вами поспорил. Делаю хобби-проект сам, делаю миграции (встроенные в Yii). После нескольких обновлений базы при деплое гораздо легче запустить миграцию автоматически, чем вспоминать и делать что-то там «руками».
                    0
                    Рассмотрите чуть более далекую перспективу, чем первый месяц-два работы над проектом. Со временем время на ручное обновление БД при деплое приложения, или приведение тестовой БД в актуальное состояние будет занимать гораздо больше времени, чем Вы секономите при написании миграции (опять же, через пару недель написание миграции занимает не больше времени, чем соответствующии инструкции SQL).
                    Это я уже не говорю о том, что при «ручном» версионировании БД практически невозможно использование CI для Вашего проекта. Да и в целом, автоматизация процесса — дело благое, в идеале нужно добиться, чтобы можно было развернуть проект одной командой в консоли.
                    К тому же, закладываясь на то, что «проект делаю сам, зачем мне лишние трудности», Вы обрекаете себя на гораздо большие проблемы, когда, в случае успеха Вашего проекта, команда неминуемо пополнится новыми людьми.
                      0
                      Возможно, что я не дошел до состояния, чтобы миграция писалась быстрее чем 3 клика в phpmyadmin.

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

                  Only users with full accounts can post comments. Log in, please.