Статьи

Phinx — миграционная библиотека, которую вы никогда не знали

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

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

Логотип Phinx

Бутстрапирование

Сначала давайте установим Phinx в проект с помощью Composer :

composer require robmorgan/phinx --dev 

Двоичный файл Phinx будет установлен в папку vendor/bin в соответствии со значениями Composer по умолчанию. Затем его можно выполнить, выполнив:

 php vendor/bin/phinx 

Phinx нужен файл phinx.yml из которого можно прочитать конфигурацию базы данных, прежде чем он сможет что-либо осмыслить. Чтобы сгенерировать его, мы запускаем:

 php vendor/bin/phinx init 

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

Сгенерированный файл будет выглядеть примерно так:

 paths: migrations: %%PHINX_CONFIG_DIR%%/db/migrations seeds: %%PHINX_CONFIG_DIR%%/db/seeds environments: default_migration_table: phinxlog default_database: development production: adapter: mysql host: localhost name: production_db user: root pass: '' port: 3306 charset: utf8 development: adapter: mysql host: localhost name: development_db user: root pass: '' port: 3306 charset: utf8 testing: adapter: mysql host: localhost name: testing_db user: root pass: '' port: 3306 charset: utf8 

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

Так обстоит дело, например, с nofw , который в предыдущей версии все еще использует Gatekeeper — безопасный, но неуклюже разработанный пакет авторизации пользователей. Для Gatekeeper phinx.yml собственный файл phinx.yml (с настраиваемым путем миграции), и он не предоставляет возможность изменить, какой из них используется, и в то же время требует наличия собственной пользовательской базы данных. Это бросает рывок в идею «давайте использовать Phinx в проекте, уже использующем Gatekeeper».

Для подобных случаев Phinx предлагает опцию -c которая сообщает программе запуска, какой файл phinx.yml использовать. Обратите внимание, что Phinx также поддерживает форматы файлов json и php , но здесь мы сосредоточимся на стандартном yml и создадим отдельный файл для нашей базы данных примера.

 mv phinx.yml my-phinx.yml 

Это просто переименовывает файл по умолчанию и освобождает место для «специальных» пакетов, таких как Gatekeeper. Было бы здорово, если бы Phinx поддерживал несколько путей миграции , но мы возьмем то, что сможем получить.

Прежде чем мы создадим первую миграцию, нам нужно заполнить учетные данные в my-phinx.yml . На Homestead Improved достаточно будет следующего:

 paths: migrations: db/migrations environments: default_migration_table: phinxlog default_database: development production: adapter: mysql host: localhost name: production_db user: username pass: 'password' port: 3306 charset: utf8 development: adapter: mysql host: localhost name: homestead user: homestead pass: 'secret' port: 3306 charset: utf8 

Первая миграция

Давайте представим, что у нас есть приложение, которому требуется следующая функциональность:

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

Модель для этого может выглядеть примерно так:

Диаграмма сущностей-отношений вышеуказанных функций

Преобразованный в SQL с MySQL Workbench, это было бы просто импортировать в базу данных:

 -- MySQL Workbench Forward Engineering SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES'; -- ----------------------------------------------------- -- Schema mydb -- ----------------------------------------------------- -- ----------------------------------------------------- -- Schema mydb -- ----------------------------------------------------- CREATE SCHEMA IF NOT EXISTS `mydb` DEFAULT CHARACTER SET utf8 ; USE `mydb` ; -- ----------------------------------------------------- -- Table `mydb`.`tag` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `mydb`.`tag` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(45) NOT NULL, `description` TEXT NULL, `context` VARCHAR(25) NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_by` INT UNSIGNED NOT NULL, `visibility` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1, PRIMARY KEY (`id`), UNIQUE INDEX `name_creator_visibile` (`created_by` ASC, `name` ASC, `visibility` ASC), INDEX `context_key` (`context` ASC)) ENGINE = InnoDB; -- ----------------------------------------------------- -- Table `mydb`.`tag_relation` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `mydb`.`tag_relation` ( `tag_id` INT UNSIGNED NOT NULL, `entity_id` INT UNSIGNED NOT NULL, `entity_type` VARCHAR(45) NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_by` INT UNSIGNED NOT NULL, PRIMARY KEY (`tag_id`, `entity_id`, `entity_type`), INDEX `tag_key` (`tag_id` ASC), INDEX `entity_key` (`entity_id` ASC, `entity_type` ASC), CONSTRAINT `tag_id_fk` FOREIGN KEY (`tag_id`) REFERENCES `mydb`.`tag` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE = InnoDB; -- ----------------------------------------------------- -- Table `mydb`.`file` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `mydb`.`file` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, `path` TEXT NOT NULL, `created_by` INT UNSIGNED NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_by` INT UNSIGNED NOT NULL, `access` VARCHAR(6) NOT NULL DEFAULT 'public', PRIMARY KEY (`id`), INDEX `creator` (`created_by` ASC)) ENGINE = InnoDB; -- ----------------------------------------------------- -- Table `mydb`.`message` -- ----------------------------------------------------- CREATE TABLE IF NOT EXISTS `mydb`.`message` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `created_by` INT UNSIGNED NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `content` TEXT NOT NULL, `attachments` TEXT NULL, PRIMARY KEY (`id`)) ENGINE = InnoDB; SET SQL_MODE=@OLD_SQL_MODE; SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; 

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

Лучшая практика

Прежде чем продолжить, давайте поговорим о лучших практиках.

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

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

Имея это в виду, давайте начнем.

Создание миграций

Сначала мы создадим таблицы.

 php vendor/bin/phinx create Tag php vendor/bin/phinx create File php vendor/bin/phinx create Message 

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

После выполнения этой команды Phinx создаст три файла в db/migrations , каждый из которых начинается с даты и времени создания и заканчивается именем миграции, например, 20160508205010_tag.php . Файлы будут расширять класс AbstractMigration и содержать код шаблона, в отличие от следующего:

 < ?php use Phinx\Migration\AbstractMigration; class Tag extends AbstractMigration { /** * Change Method. * * Write your reversible migrations using this method. * * More information on writing migrations is available here: * http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class * * The following commands can be used in this method and Phinx will * automatically reverse them when rolling back: * * createTable * renameTable * addColumn * renameColumn * addIndex * addForeignKey * * Remember to call "create()" or "update()" and NOT "save()" when working * with the Table class. */ public function change() { } } 

Обратите внимание, что, хотя Phinx поддерживает стандартные методы up и down которые вы привыкли видеть в других инструментах миграции, по умолчанию он change которые могут автоматически выполнять обратную миграцию, устраняя необходимость в написании отдельных процедур.

Давайте теперь создадим миграцию тегов, изменив метод change() так:

  public function change() { $tag = $this->table('tag'); $tag ->addColumn('name', 'string', ['limit' => 45, 'null' => false]) ->addColumn('description', 'text') ->addColumn('context', 'string', ['limit' => 25]) ->addColumn('created', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) ->addColumn('created_by', 'integer', ['signed' => false, 'null' => false]) ->addColumn('visibility', 'boolean', ['null' => false, 'signed' => false, 'default' => 1]) ; $tag->addIndex(['name', 'created_by', 'visibility'], ['unique' => true, 'name' => 'name_creator_visible']); $tag->addIndex(['context']); $tag->create(); $tagRelation = $this->table('tag_relation', array('id' => false, 'primary_key' => array('tag_id', 'entity_id', 'entity_type'))); $tagRelation ->addColumn('tag_id', 'integer', ['null' => false]) ->addColumn('entity_id', 'integer', ['null' => false, 'signed' => false]) ->addColumn('entity_type', 'string', ['limit' => 45, 'null' => false]) ->addColumn('created', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) ->addColumn('created_by', 'integer', ['signed' => false, 'null' => false]) ; $tagRelation->addIndex(['tag_id']); $tagRelation->addIndex(['entity_id', 'entity_type'], ['name' => 'entity']); $tagRelation->addForeignKey('tag_id', 'tag', 'id', array('delete'=> 'CASCADE', 'update'=> 'NO_ACTION')); $tagRelation->create(); } 

Разбивая его, мы сначала определяем таблицу tag , затем добавляем все столбцы в соответствии с документацией и, наконец, добавляем туда некоторые индексы для хорошей меры. Обратите внимание, что в таблице tag нет столбца id . Это связано с тем, что Phinx автоматически создает первичный ключ с автоматическим увеличением, который называется id если не указано иное.

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

Далее мы tag_relation таблицу tag_relation . Как упоминалось ранее, Phinx создает автоинкрементный первичный ключ с именем id , поэтому мы должны отключить его в первоначальном определении таблицы. В той же строке мы определяем альтернативный первичный ключ, состоящий из tag_id , entity_id и entity_type . Это гарантирует, что к данному объекту может быть прикреплена только одна копия одного тега. Добавление столбцов происходит как обычно, а затем пришло время снова создавать индексы. tag_id позволяет быстро найти все объекты с данным тегом, а комбинация entity_id и entity_type позволяет нам быстро перечислить все теги данного объекта.

Наконец, мы создаем простой внешний ключ, привязывая поле tag_id полю id таблицы tag , чтобы строки tag_relation соответствующие указанному tag_id удалялись, если сам тег удалялся из системы.

Наша первоначальная миграция тегов теперь готова. Давайте проверим это. Мы начинаем миграцию с:

 php vendor/bin/phinx migrate -c my-phinx.yml 

При желании мы можем -e X флаг -e X где X — среда, на которую мы нацелены. В этом случае в этом нет необходимости, поскольку наш файл my-phinx.yml отмечает development в качестве базы данных по умолчанию (и, следовательно, среды по умолчанию).

Конечно же, после выполнения наши таблицы там:

CLI вывод успешного выполнения миграции

Таблицы видны в SequelPro

Давайте быстро запишем file и миграцию message .

 // File public function change() { $file = $this->table('file'); $file ->addColumn('name', 'string', ['limit' => 255, 'null' => false]) ->addColumn('path', 'text', ['null' => false]) ->addColumn('access', 'string', ['limit' => 6, 'null' => false, 'default' => 'public']) ->addColumn('created', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) ->addColumn('created_by', 'integer', ['signed' => false, 'null' => false]) ->addColumn('updated', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) ->addColumn('updated_by', 'integer', ['signed' => false, 'null' => false]) ; $file->addIndex(['created_by'], ['name' => 'creator']); $file->addIndex(['access'], ['name' => 'accessibility']); $file->create(); } 
 // Message public function change() { $message = $this->table('message'); $message ->addColumn('content', 'text', ['null' => false]) ->addColumn('attachments', 'text') ->addColumn('created', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) ->addColumn('created_by', 'integer', ['signed' => false, 'null' => false]) ; $message->addIndex(['created_by'], ['name' => 'creator']); $message->create(); } 

Исправление проблем

Однако если мы сейчас запустим команду migrate, то заметим, что ничего не происходит. Новые базы данных не появляются. Что дает?

Когда Phinx выполнил миграцию tag , он сделал запись в таблице phinxlog в базе данных, отметив, какая миграция была выполнена в последний раз. Поскольку файлы миграции для Message и File уже существовали на тот момент (но были пустыми), они были помечены как перенесенные и, таким образом, игнорируются в этом запуске, поскольку этот запуск «официально» уже выполнен. Сначала мы должны откатиться . Откат отменяет последнюю миграцию.

 php vendor/bin/phinx rollback -c my-phinx.yml 

Сообщение об ошибке неудачного отката о том, что не удается найти нужную таблицу

Ой! Что теперь?

Что ж, при переносе файлов файлы переноса файлов и сообщений были пустыми. Phinx читает метод change миграции, чтобы выяснить процесс отмены (например, он превращает create table в drop table ), и, поскольку теперь он находит упоминание таблиц в файлах, где раньше их не было, он запутался — нет такой стол!

Есть несколько способов обойти эту проблему:

  1. Будьте осторожны при написании миграций. Либо запишите их все сразу, а затем перенесите, либо создайте и напишите по одному и перенесите после завершения каждого из них.
  2. Прокомментируйте содержимое двух новых методов change : сохраните, выполните откат, затем раскомментируйте.
  3. Вручную удалите phinxlog , tag и tag_relation поскольку мы настраиваем их впервые и не можем нанести никакого ущерба.
  4. Используйте методы up и down вместо change — тогда будет использоваться метод down при откате, который может быть либо пустым, либо содержать одну команду drop table .

Мы пойдем с вариантом 2.

Сначала прокомментируйте содержимое обоих методов change . Затем запустите:

 php vendor/bin/phinx rollback -c my-phinx.yml -t XXXXXXXX 

… Где XXXXXXXX — это число перед миграцией, на которое вы хотите откатиться, если вы хотите пропустить несколько из них. Если пропустить число, оно просто возвращается один раз к последней известной миграции, поэтому запуск его несколько раз без -t также помогает.

Наконец, мы можем запустить migrate и импортировать ее. Во-первых, мы раскомментируем методы change . Потом:

 php vendor/bin/phinx migrate -c my-phinx.yml 

Успешная миграция всех трех файлов

Успех! Все три таблицы были созданы!

Последующие миграции и управление версиями

Итак, как мы делаем изменения и последующие миграции сейчас? Допустим, мы хотим, чтобы таблица message :

  • содержит поле, которое может зарегистрировать все значения user_id учетных записей пользователей, которые видели сообщение.
  • содержать предмет, а не только тело.
  • быстрый поиск по теме и по телу (индекс FULLTEXT — только в MySQL)

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

Мы создаем новую миграцию:

 php vendor/bin/phinx create MessageSeen -c my-phinx.yml 

Во вновь созданном классе message_seen мы change метод change на:

  public function change() { $message = $this->table('message'); $message->addColumn('seen_by', 'text'); $message->addColumn('subject', 'text'); if ($this->getAdapter()->getAdapterType() === 'mysql') { $message->addIndex('subject', ['type' => 'fulltext']); $message->addIndex('content', ['type' => 'fulltext']); } $message->update(); } 

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

 php vendor/bin/phinx migrate -c my-phinx.yml 

Конечно же, изменения есть:

Дополнительные изменения были применены к таблице и теперь видны в базе данных.

Вывод

Phinx — это очень простой и простой в использовании пакет с функциональностью, аналогичной функциональности некоторых платформ, но полностью независимой. Это дает нам возможность создавать, уничтожать и изменять таблицы базы данных без написания необработанного SQL, что не только облегчает написание и версионирование изменений, но также позволяет нам позже переключить базовый адаптер базы данных, скажем, с MySQL на Postgre!

Какая самая сложная миграция вы когда-либо писали? Ожидаете ли вы каких-либо сбоев с этим подходом? Дайте нам знать об этом в комментариях!