![]() |
Технология Клиент-Сервер 2007'3 |
||||||
|
ПРИМЕЧАНИЕ Если вы не знакомы с 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-фреймворки, как Hibernate, защищают вас от многих таких повторений путем широкого использования генерирования кода. В то время как объектно-реляционные отображения позволяют вам работать с традиционными схемами, для новых схем баз данных вы можете сгенерировать схему непосредственно из вашей модели при помощи предоставляемых в Hibernate инструментальных средств, а также можете сгенерировать getter- и setter-методы при помощи IDE. Вы можете встроить ваше отображение в предметную модель (по моему мнению, частично аннулируя первичное предназначение карты) при помощи Java-аннотаций. Такая методика генерирования кода также служит еще одной цели – миграции схемы. Некоторые из этих инструментальных средств генерирования кода могут обнаруживать различия между вашей новой предметной моделью и старой схемой, а также генерировать SQL-сценарии для преодоления этих различий. Помните о том, что эти сценарии имеют дело со схемой, но не с данными.
Например, рассмотрим миграцию, которая объединяет столбцы базы данных first_name и last_name в один столбец с названием name. Инструментальные средства типичного Java-фреймворка, поддерживающего персистентность, не помогут администратору БД, поскольку они работают только с одной частью проблемы – изменениями в схеме. Когда вы делаете такое изменение схемы, вам необходимо также работать с существующими данными. Когда приходит время развернуть новую версию этого гипотетического приложения, администратор базы данных обычно должен вручную создать SQL-сценарии для выполнения следующих операций:
Если версия кода, вызывающая изменение схемы, чем-то нехороша, изменения часто приходится откатывать (roll back) вручную. Лишь немногие разработчики практикуют интегрирование и автоматизацию изменений модели, схемы и данных.
В Rails все изменения схемы, включая ее начальное создание, выполняются с помощью миграций (migration). Каждое изменение в схеме базы данных имеет свой собственный объект migration, инкапсулирующий движение "вверх" (up) и "вниз" (down). В листинге 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-проекта, использующего БД, выполните следующие действия:
Чтобы увидеть, как работает нумерация, сгенерируйте миграцию:
> 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:
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):
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 четко отслеживает номер текущей версии. Такой подход отлично работает, позволяя плавно перемещаться по схемам, представляющим различные стадии процесса разработки.
> 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:
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:
def up create_table :articles, :id => false do |t| ... end end |
Детально исследовав одиночную миграцию, мы пока что не сделали ни одного изменения в схеме. Настало время создать еще одну таблицу, на этот раз для комментариев. Сгенерируйте модель с названием Comment, выполнив команду script/generate model Comment. Измените полученную миграцию в db/migrate/002_create_comments.rb так, как показано в листинге 8. Вам понадобится новая таблица с парой столбцов, и вы также воспользуетесь преимуществом способности Rails добавлять не пустые столбцы и значения по умолчанию.
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 показан результат:
> 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.
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 показана эта миграция:
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 ООО "К-Пресс"