Продолжается подписка на наши издания! Вы не забыли подписаться?

Пересекая грани: Rails-миграции

Автор: Брюс Тэйт
RapidRed
Опубликовано: 20.10.2008

ПРИМЕЧАНИЕ

Если вы не знакомы с Ruby on Rails, настоятельно рекомендуем сначала прочесть публикуемые в этом номере журнала главы из книги Build Your Own Ruby On Rails Web Applications Патрика Ленза, подробно рассказывающие о том, что же такое Ruby on Rails.

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

В настоящее время фреймворки, обеспечивающие персистентность, используют один из двух подходов: отображение (mapping) или обертывание (wrapping). Отображение позволяет создавать независимые схемы базы данных и объектные модели, а затем использовать один уровень программного обеспечения для управления различиями между ними. Решения с отображением стремятся к созданию объектной модели, которая практически совпадает со структурой схемы базы данных. В отличие от этого, решения с обертыванием для управления данными в базе данных используют объекты как обертки вокруг таблиц и строк базы данных. Считается, что часто отображение является более гибким, поскольку использующее его программное обеспечение может лучше справиться с изменениями в схеме или объектной модели. Но это мнение игнорирует самую важную часть уравнения: данные. Для эффективного управления любым изменением приложения, связанным с моделью персистентности, вы должны скоординировать изменения в данных, схеме и модели. Большинство разработчиков не до конца понимают это.

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

В данной статье детально рассматриваются миграции (migrations) Ruby on Rails – Rails-решение для работы с изменениями в рабочей базе данных. Миграции объединяют мощь и простоту для координирования изменений схемы и изменений данных, используя подход с обертыванием.

Управление изменениями схемы в Java-программировании

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

По правде говоря, такие Java-фреймворки, как Hibernate, защищают вас от многих таких повторений путем широкого использования генерирования кода. В то время как объектно-реляционные отображения позволяют вам работать с традиционными схемами, для новых схем баз данных вы можете сгенерировать схему непосредственно из вашей модели при помощи предоставляемых в Hibernate инструментальных средств, а также можете сгенерировать getter- и setter-методы при помощи IDE. Вы можете встроить ваше отображение в предметную модель (по моему мнению, частично аннулируя первичное предназначение карты) при помощи Java-аннотаций. Такая методика генерирования кода также служит еще одной цели – миграции схемы. Некоторые из этих инструментальных средств генерирования кода могут обнаруживать различия между вашей новой предметной моделью и старой схемой, а также генерировать SQL-сценарии для преодоления этих различий. Помните о том, что эти сценарии имеют дело со схемой, но не с данными.

Например, рассмотрим миграцию, которая объединяет столбцы базы данных first_name и last_name в один столбец с названием name. Инструментальные средства типичного Java-фреймворка, поддерживающего персистентность, не помогут администратору БД, поскольку они работают только с одной частью проблемы – изменениями в схеме. Когда вы делаете такое изменение схемы, вам необходимо также работать с существующими данными. Когда приходит время развернуть новую версию этого гипотетического приложения, администратор базы данных обычно должен вручную создать SQL-сценарии для выполнения следующих операций:

Если версия кода, вызывающая изменение схемы, чем-то нехороша, изменения часто приходится откатывать (roll back) вручную. Лишь немногие разработчики практикуют интегрирование и автоматизацию изменений модели, схемы и данных.

Основы Rails-миграций

В Rails все изменения схемы, включая ее начальное создание, выполняются с помощью миграций (migration). Каждое изменение в схеме базы данных имеет свой собственный объект migration, инкапсулирующий движение "вверх" (up) и "вниз" (down). В листинге 1 показана пустая миграция:

Листинг 1. Пустая миграция

class EmptyMigration < ActiveRecord::Migration
  def self.up
  end

  def self.down
  end
end

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

Инкапсулируя up и down, инструментальные средства Rails-разработки и рабочие программы могут автоматизировать процесс развертывания и отмены любого изменения, затрагивающего персистентные объектные модели.

Разрешая изменения данных, миграции значительно облегчают синхронизацию изменений в данных и схеме, которые часто происходят одновременно. Например, вы можете добавить новую справочную таблицу (lookup table), связывающую каждый штат и его двузначный ZIP-код. С помощью миграции вы можете заполнить таблицу базы данных, возможно, активизируя SQL-сценарий или загружая константы. Если ваши миграции корректны, каждая из них оставляет вашу базу данных в непротиворечивом состоянии без ручного вмешательства.

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

Использование миграций

Для использования миграций нужен только Rails-проект и база данных. Если вы хотите использовать приведенный в статье код, установите РСУБД, а также Ruby и Rails версии 1.1 или старше. После этого все готово к работе. Для создания Rails-проекта, использующего БД, выполните следующие действия:

  1. Создайте Rails-проект с названием blog, введя rails blog.
  2. Создайте базу данных с названием blog_development. Используя MySQL, я просто ввел команду create database blog_development в командной строке MySQL.
  3. Настройте базу данных, как указано в config/database.yml, добавив ваш логин и пароль к базе данных.

Чтобы увидеть, как работает нумерация, сгенерируйте миграцию:

  1. Из каталога blog введите команду ruby script/generate migration create_blog. Если вы работаете в Unix, то можете опустить ruby. Именно так я и буду делать с этого момента.
  2. Введите команду script/generate migration create_user. Взгляните на файлы в blog/db/migrate. Вы увидите два последовательно пронумерованных файла. Этими номерами управляет генератор миграции.
  3. Удалите миграцию с названием 001_create_blog.rb и создайте ее снова при помощи команды script/generate migration create_blog. Вы обнаружите, что новая миграция создалась как 003_create_blog.rb (см. листинг 2):

Листинг 2. Генерирование миграций

> cd blog
> script/generate migration create_blog
      create  db/migrate
      create  db/migrate/001_create_blog.rb
> script/generate migration create_user
      exists  db/migrate
      create  db/migrate/002_create_user.rb
> ls db/migrate/  
001_create_blog.rb      002_create_user.rb
> rm db/migrate/001_create_blog.rb 
> script/generate migration create_blog
      exists  db/migrate
      create  db/migrate/003_create_blog.rb
> ls db/migrate/
002_create_user.rb      003_create_blog.rb

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

Чтобы увидеть, как работают миграции в базе данных, удалите все миграции, имеющиеся в каталоге db/migrations. Сгенерируйте объект модели для Article и пустую миграцию, как, например, в листинге 1, выполнив команду script/generate model Article. Измените db/migrate/001_create_articles.rb так, как показано в листинге 3:

Листинг 3. Миграция для CreateArticles

class CreateArticles < ActiveRecord::Migration
  def self.up
    create_table :articles do |t|
      t.column :name, :string, :limit => 80
      t.column :author, :string, :limit => 40
      t.column :body, :text
      t.column :created_on, :datetime
    end
  end

  def self.down
    drop_table :articles
  end
end

Миграция вверх и вниз

Чтобы увидеть, что на самом деле делает миграция, просто запустите ее и посмотрите на базу данных. Из каталога blog выполните команду rake migrate. rake – это Ruby-эквивалент программы make в C или программы ant платформы Java. migrate – это одно задание rake.

Затем посмотрите на таблицы БД. В MySQL просто перейдите в командную строку mysql>, выполните команды blog_development; и show tables; (см. листинг 4):

Листинг 4. Таблица schema_info, созданная Rails-миграциями

mysql> show tables;
+----------------------------+
| Tables_in_blog_development |
+----------------------------+
| articles                   |
| schema_info                |
+----------------------------+
2 rows in set (0.00 sec)

mysql> select * from schema_info;
+---------+
| version |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)

Обратите внимание на вторую таблицу, schema_info. В миграции была указана только таблица articles, но команда rake migrate также автоматически создала schema_info. Выполните команду select * from schema_info.

При выполнении команды rake migrate без параметров вы указываете Rails выполнить все миграции, которые еще не были применены. Rails выполняет следующее:

Для миграции "вниз" просто выполните команду rake migrate с номером версии. При этом могут быть удалены данные, так что будьте внимательны. Некоторые операции, такие как удаление таблиц или столбцов, тоже удаляют данные. В листинге 5 показаны результаты миграции "вниз", а затем назад "вверх". Вы можете увидеть, что schema_info четко отслеживает номер текущей версии. Такой подход отлично работает, позволяя плавно перемещаться по схемам, представляющим различные стадии процесса разработки.

Листинг 5. Миграция вниз

> rake migrate VERSION=0
(in /Users/batate/rails/blog)
== CreateArticles: reverting ==================================================
-- drop_table(:articles)
   -> 0.1320s
== CreateArticles: reverted (0.1322s) =========================================

> mysql -u root blog_development;
mysql> show tables;
+----------------------------+
| Tables_in_blog_development |
+----------------------------+
| schema_info                |
+----------------------------+
1 row in set (0.00 sec)

mysql> select * from schema_info;
+---------+
| version |
+---------+
|       0 |
+---------+
1 row in set (0.00 sec)

mysql> exit
Bye
> rake migrate
(in /Users/batate/rails/blog)
== CreateArticles: migrating ==================================================
-- create_table(:articles)
   -> 0.0879s
== CreateArticles: migrated (0.0881s) =========================================

> mysql -u root blog_development;
mysql> select * from schema_info;
+---------+
| version |
+---------+
|       1 |
+---------+
1 row in set (0.00 sec)

Теперь давайте откроем саму таблицу. Посмотрите опять на листинг 3 и определение таблицы. В MySQL вы можете выполнить команду show create table articles;, которая отображает информацию, показанную в листинге 6:

Листинг 6. Определение таблицы для articles

                
mysql> show create table articles;
+----------+...-----------------+
| Table    | Create Table |
+----------+...-----------------+
| articles | CREATE TABLE 'articles' (
  'id' int(11) NOT NULL auto_increment,
  'name' varchar(80) default NULL,
  'author' varchar(40) default NULL,
  'body' text,
  'created_on' datetime default NULL,
  PRIMARY KEY  ('id')
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+----------+...-----------------+
1 row in set (0.00 sec)

Вы можете увидеть, что большая часть этого определения таблицы пришла непосредственно из миграции. Одним из ключевых преимуществ Rails-миграций является то, что вам не нужно напрямую использовать синтаксис SQL для создания таблиц. Вы обрабатываете каждое изменение схемы в Ruby, в результате чего генерируемый SQL не зависит от базы данных. Но обратите внимание на столбец id. Хотя вы не указывали этот столбец, Rails-миграция все равно его создала за вас с параметрами auto_increment и NOT NULL. Столбец id с этим конкретным определением следует соглашению Rails для столбца с идентификатором. Если бы вы хотели создать эту таблицу без id, ваша миграция просто добавила бы параметр :id => false, как, например, в листинге 7:

Листинг 7. Создание таблицы без столбца id

  def up
    create_table :articles, :id => false do |t| 
      ...
    end
  end

Детально исследовав одиночную миграцию, мы пока что не сделали ни одного изменения в схеме. Настало время создать еще одну таблицу, на этот раз для комментариев. Сгенерируйте модель с названием Comment, выполнив команду script/generate model Comment. Измените полученную миграцию в db/migrate/002_create_comments.rb так, как показано в листинге 8. Вам понадобится новая таблица с парой столбцов, и вы также воспользуетесь преимуществом способности Rails добавлять не пустые столбцы и значения по умолчанию.

Листинг 8. Вторая миграция, для комментариев

class CreateComments < ActiveRecord::Migration
  def self.up
    create_table :comments do |t|
      t.column :name, :string, :limit => 40, :null => false
      t.column :body, :text
      t.column :author, :string, :limit => 40, :default => 'Anonymous coward'
      t.column :article_id, :integer
    end
  end

  def self.down
    drop_table :comments
  end
end

Выполните эту миграцию. Если у вас при выполнении миграции возникает ошибка, просто вспомните, как работает миграция. Вы должны проверить значение строки в schema_info и посмотреть на состояние базы данных. Возможно, вам придется удалить некоторые таблицы вручную или изменить значение строки в schema_info после исправления вашего кода. Помните, никаких чудес не происходит. Rails выполняет методы up всех миграций, которые еще не запускались. Если вы добавляете уже существующую таблицу или столбец, операция потерпит неудачу, поэтому нужно проверить, что ваша миграция находится в непротиворечивом состоянии. А сейчас выполните команду rake migrate. В листинге 9 показан результат:

Листинг 9. Выполнение второй миграции

> rake migrate(in /Users/batate/rails/blog)
== CreateComments: migrating ==================================================
-- create_table(:comments)
   -> 0.0700s
== CreateComments: migrated (0.0702s) =========================================

> mysql -u root blog_development;
mysql> select * from schema_info;
+---------+
| version |
+---------+
|       2 |
+---------+
1 row in set (0.00 sec)

Миграции могут обрабатывать различные типы изменений схем. Вы можете добавлять и удалять индексы, изменять таблицы, удаляя, переименовывая или добавляя столбцы, и даже выполнить SQL-выражение при необходимости. Вы можете выполнить в миграции все, что можете сделать в SQL. Rails имеет обертки (wrappers) для большинства обычных операций, включая:

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

Листинг 10. Миграция, создающая таблицу и добавляющая столбец

class CreateBlogs < ActiveRecord::Migration
  def self.up
    create_table :blogs do |t|
      t.column :name, :string, :limit => 40;
    end
    add_column "articles", "blog_id", :integer
  end

  def self.down
    drop_table :blogs
    remove_column "articles", "blog_id"
  end
end

И данные тоже

Вы можете сделать в миграции все, что можете сделать в SQL.

До сих пор я рассматривал только изменения в схеме, но изменения в данных тоже важны. Некоторые изменения в базе данных требуют изменения данных вместе с изменениями схемы, а некоторые из этих изменений данных требуют логических изменений. Допустим, вы хотите создать новый комментарий в каждой статье блога для указания того, что статья открыта для комментариев. Если ваш блог уже был какое-то время открыт, вы, реализуя изменения, захотите добавлять этот комментарий только в те статьи, которые еще не имеют комментариев. Вы можете легко сделать это изменение с помощью миграции, поскольку миграция имеет доступ к объектам модели и может принимать логические решения на основе состояния модели. Выполните команду script/generate migration add_open_for_comments. Вы должны изменить comment для отражения отношения belongs_to и создать новую миграцию. В листинге 11 показана эта миграция:

Листинг 11. Объект модели и новая миграция

class AddOpenForComments < ActiveRecord::Migration
  def self.up
    Article.find_all.each do |article|
      if article.comments.size == 0
        Comment.new do |comment|
          comment.name = 'Welcome.'
          comment.body = "Article '#{article.name}' is open for comments."
          article.comments << comment
          comment.save
          article.save
        end
      end
    end
  end

  def self.down
  end
end

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

Я показал вам большую часть того, что доступно в миграциях. Вы имеете в своем распоряжении и несколько других инструментальных средств. Если вы хотите начать использовать миграции с существующей базой данных, то можете сделать снимок вашей существующей схемы при помощи команды rake schema_dump. Это задание rake создает Ruby-схему с корректным синтаксисом миграций в db/schema.rb. Вы можете затем сгенерировать миграцию и скопировать схему, которую вы выгрузили в миграцию (см. раздел "Ресурсы" для получения более подробной информации). Я также не рассказал о тестовых средствах, которые могут быть полезны в настройке тестовых данных или наполнении базы данных. Более подробную информацию вы можете получить в одной из моих более ранних статьей серии "Пересекая границы", посвященной модульным тестам.

Окончательное сравнение

Миграции в Java-программировании не такие цельные. Некоторые продукты используют отдельные решения некоторых проблем миграций схем, но без системного подхода для координации изменений в схеме (и "вверх", и "вниз") работа с изменениями в данных и модели объекта может быть трудной задачей. Rails-решения обладают следующими основными преимуществами:

Судя по всем этим преимуществам, можно было бы ожидать использования сложного кода, но на самом деле миграции очень просты. Они имеют понятные имена и номера версий. Каждая миграция имеет методы up и down. Наконец, задание rake координирует их выполнение в определенном порядке. Эта простая стратегия тоже является революционной. Идея выражать каждое изменение схемы не в модели, а как отдельную миграцию, является сколь элегантной, столь и эффективной. Но самое интересное заключается в том, что эти идеи совершенно не зависят от языка программирования. Если вы создаете новый Java-фреймворк, было бы отличным решением реализовать миграции.

Ресурсы

Научиться

Получить продукты и технологии


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

Copyright © 1994-2016 ООО "К-Пресс"