Миграции базы данных в Rails

Frederick Cheung, “Rails Database Migrations”, public translation into Russian from English More about this translation.

Translate into another language.

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

Active Record отслеживает какие миграции были уже применены, вам всего лишь надо обновить исходные тексты и запустить rake db:migrate, а Active Record сам выполнит миграции, которые должны быть применены. В процессе выполнения будет также обновлен файл db/schema.rb, чтобы соответствовать структуре базы данных.

Миграции позволяют описать преобразования на языке Ruby. Важным является (как и для большинства функционала Active Record) независимость от базы данных: нет нужды помнить специфический синтаксис CREATE TABLE или SELECT * (хотя можно опуститься до уровня "сырого" SQL для конкретной базы данных). Например, можно использовать SQLite3 в процессе разработки, а MySQL в промышленной среде.

Мы изучим всё о миграциях, в том числе:

Генераторы для их создания.

Методы Active Record для изменения базы данных.

Rake-задачи для управления ими.

Как они влияют на schema.rb

Анатомия миграций

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

class CreateProducts < ActiveRecord::Migration
def self.up
create_table :products do |t|
t.string :name
t.text :description
t.timestamps
end
end
def self.down
drop_table :products
end
end

Эта миграция создает таблицу Products со строковым столбцом name и текстовым столбцом description. По-умолчанию, также добавляется первичный ключ с именем id (это можно явно не указывать). Автоматически будут созданы столбцы отметок времени created_at и updated_at. Откат этой миграции состоит в простом удалении таблицы.

Миграции не ограничены только изменениями схемы. С их помощью можно исправлять некорректные данные в базе или заполнять новые поля:

class AddReceiveNewsletterToUsers < ActiveRecord::Migration
def self.up
change_table :users do |t|
t.boolean :receive_newsletter, :default => false
end
User.update_all ["receive_newsletter = ?", true]
end
def self.down
remove_column :users, :receive_newsletter
end
end

Эта миграция добавляет столбец receive_newsletter в таблице users. Для новых пользователей значение по-умолчанию устанавливается в false, но для существующих пользователей принято решение указать значение true, поэтому мы используем модель User для задания этого значения существующим пользователям.

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

1.1 Миграции -- это Классы

Миграция -- это подкласс ActiveRecord::Migration, содержащий два метода: up (выполняет заданные изменения в БД) и down (откатывает изменения назад)

Active Record предоставляет независимые от типа БД методы для основных операций с базой данных (что будет описано ниже):

create_table (создать таблицу)

change_table (изменить таблицу)

drop_table (удалить таблицу)

add_column (добавить колонку)

change_column (изменить колонку)

rename_column (переименовать колонку)

remove_column (удалить колонку)

add_index (добавить индекс)

remove_index (удалить индекс)

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

Для баз данных с поддержкой транзакций выражения, меняющие схему, выполняются в рамках транзакции (например, в PostreSQL). Если в базе данных нет поддержки транзакций (MySQL или SQLite), то в случае ошибки на каком-то из этапов миграции откат не будет осуществлятся. Нужно будет делать откат вручную.

1.2. Что в имени миграции

Миграции хранятся в файлах в db/migrate по одному файлу на класс миграции. Имя файла имеет вид YYYYMMDDHHMMSS_create_products.rb, в котором используется UTC-время в качестве уникального идентификатора и через знак подчеркивания указывается имя миграции. Имя класса миграции должно соответствовать второй части имени файла (в CamelCase-виде). Например, в файле 20080906120000_create_products.rb должен присутствовать класс CreateProducts, а в 20080906120001_add_details_to_products.rb - класс AddDetailsToProducts. Имя файла нужно менять одновременно с именем класса в этом файле, чтобы Rails не сообщал об отстутствующем классе.

Rails использует для своих нужд только отметку времени в качестве уникального номера миграции. До Rails 2.1 номера миграций начинались с 1 и увеличивались по мере создания новых миграций. При увеличении числа разработчиков легко столкнуться с необходимостью откатывать миграции и делать перенумерацию. С выходом Rails 2.1 эта проблема во многом решена использованием времени в качестве номера миграции. Можно перейти на старую схему нумерации миграций, установив в config/environment.rb config.active_record.timestamped_migrations =false

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

Например: Алиса добавила миграции 20080906120000 и 20080906123000, а Вова добавил 20080906124500 и запустил ее. Алиса завершила вносить изменения в миграции и подлила их, а Вова выгрузил себе последние изменения. Rails знает, что две миграции от Алисы не отрабатывали, так что rake db:migrate запустит их (даже несмотря на то, что миграция от Вовы с более поздним временем уже отработала), и аналогично, откат миграций не вызовет их down-методов.

Естественно, такой процесс не заменяет общение внутри команды. Если миграция Алисы удалила таблицу, которую Вова в своей миграции считает существующей, обязательно будет конфликт.

1.3. Изменение миграций

Невозможно обойтись без ошибок в миграциях. Если такая миграция отработала, то невозможно просто отредактировать тект и перезапустить миграцию: Rails посчитает, что миграция выполнялась и не будет ничего делать во время db:migrate. Сначала необходимо откатить миграцию (rake db:rollback), исправить ошибки и только потом повторно запустить rake db:migrate.

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

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

2.1. Создание модели

Генераторы моделей и обвязок (scaffold) создают миграции в соответствии с добавленными моделями. Такие миграции уже будут содержать необходимые инструкции для корректного создания таблиц. Если мы указываем перечень необходимых столбцов, для их добавления будут созданы выражения. Так, запуск:

ruby script/generate model Product name:string description:text

создаст миграцию, содержащую:

class CreateProducts < ActiveRecord::Migration

def self.up
create_table :products do |t|
t.string :name
t.text :description
t.timestamps
end
end

def self.down
drop_table :products
end
end

Можно добавить сколько угодно пар имя_столбца/тип. По-умолчанию, добавляется вызов t.timestamps (который создает поля updated_at и created_at, автоматически заполняемые Active Record-ом).

2.2. Создание отдельной миграции

Если необходимо создать миграцию для каких-то других целей (к примеру, добавление столбца в существующую таблицу), можно использовать генератор миграций:

ruby script/generate migration AddPartNumberToProducts

В результате будет создана пустая миграция с корректным именем:

class AddPartNumberToProducts < ActiveRecord::Migration

def self.up
end

def self.down
end

end

Если имя миграции имеет вид AddXXXToYYY или RemoveXXXFromYYY и имеет следом перечень имен полей и типов, то в миграцию будут добавлены соответствующие add_column и remove_column выражения.

ruby script/generate migration AddPartNumberToProducts part_number:string

выдаст

class AddPartNumberToProducts < ActiveRecord::Migration

def self.up
add_column :products, :part_number, :string
end

def self.down
remove_column :products, :part_number
end

end

аналогично,

ruby script/generate migration RemovePartNumberFromProducts part_number:string

создаст

class RemovePartNumberFromProducts < ActiveRecord::Migration

def self.up
remove_column :products, :part_number
end

def self.down
add_column :products, :part_number, :string
end

end

Нет ограничений на число автоматически создающихся столбцов:

ruby script/generate migration AddDetailsToProducts part_number:string price:decimal

сгенерирует

class AddDetailsToProducts < ActiveRecord::Migration

def self.up
add_column :products, :part_number, :string
add_column :products, :price, :decimal
end

def self.down
remove_column :products, :price
remove_column :products, :part_number
end

end

И как обычно, сгенерированный код является только основой. Можно добавлять и удалять своё в соответствии с потребностями.

3. Написание миграций

После того, как миграция создана с помощью генератора, пришло время поработать!

3.1. Создание таблицы

Метод миграции create_table является одной из рабочих лошадок. Вот пример типового использования:

create_table :products do |t|
t.string :name
end

создается таблица products со столбцом name и (как будет обсуждаться ниже) не явно определенным столбцом id.

Объект, передаваемый внутрь блока, позволяет создавать столбцы в таблице. Есть два варианта синтаксиса. Традиционный в виде:

create_table :products do |t|
t.column :name, :string, :null => false
end

И второй, более привлекательный, избавляющий от излишнего метода column. Вместо этого используются методы string, integer и т.п., которые создают колонки соотвествующего типа. Остальные параметры методов аналогичны традиционному варианту.

create_table :products do |t|
t.string :name, :null => false
end

По-умолчанию, create_table создает колонку первичного ключа с именем id. Можно изменить имя первичного ключа, указав параметр :primary_key (не забудьте обновить связанную модель), или если первичный ключ не нужен (например, для объединительных таблиц HABTM), можно указать :id => false. Если необходимо передать базе данных специфические параметры, можно поместить фрагмент SQL в параметре :options:

create_table :products, :options => "ENGINE=BLACKHOLE" do |t|
t.string :name, :null => false
end

В SQL выражение создания таблицы будет добавлено ENGINE=BLACKHOLE (в MySQL по-умолчанию используется ENGINE=InnoDB).

Active Record поддерживает следующие типы:
:primary_key, :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean.

Они будут проецироваться на соответствующие встроенные типы в базе данных, например для MySQL :string будет отображен на varchar(255). Можно указывать не поддерживаемые Active Record типы, используя традиционный способ записи:

create_table :products do |t|
t.column :name, 'polygon', :null => false
end

Но такой вариант усложняет перенос на другие базы данных.

3.2. Изменение таблиц

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

change_table :products do |t|
t.remove :description, :name
t.string :part_number
t.index :part_number
t.rename :upccode, :upc_code
end

Здесь удаляются столбцы description и name, добавляется столбец part_number и создается индекс по нему. И в конце переименовывается столбец upccode. Того же самого можно достигнуть вызовами:

remove_column :products, :description
remove_column :products, :name
add_column :products, :part_number, :string
add_index :products, :part_number
rename_column :products, :upccode, :upc_code

Первый вариант позволяет избежать повторного указания имени таблицы и группирует выражения, относящиеся к одной таблице. Имена отдельных преобразований также сокращены: так remove_column стало просто remove, add_index -- просто index.

3.3. Специальные помощники

Active Record предоставляет ряд сокращений для часто используемых функций. Как то: очень часто необходимо одновременное использование created_at и updated_at столбцов, и поэтому для этого есть свой метод:

create_table :products do |t|
t.timestamps
end

Таблица products создастся с этими двумя столбцами (не считая id).

change_table :products do |t|
t.timestamps
end

добавляет эти два столбца к существующей таблице.

Другие помощники называются references (доступны как и belongs_to). В их простейшей форме просто улучшают читаемость:

create_table :products do |t|
t.references :category
end

создает столбец category_id соответствующего типа. Обратите внимание, что передается имя модели, а не имя столбца. Active Record добавит _id автоматически. Если есть полиморфная ассоциация belongs_to, при вызове references будут добавлены оба необходимых столбца:

create_table :products do |t|
t.references :attachment, :polymorphic => {:default => 'Photo'}
end

добавляет столбцы attachment_id и attachment_type со значением по-умолчанию "Photo".

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

Если помощников из комплекта Active Record не достаточно, можно вызвать функцию execute для выполнения произвольного SQL.

Pages: ← previous Ctrl next
1 2

© Frederick Cheung

Original (English): Rails Database Migrations

Translation: © and_rew, alexbaumgertner, GremL1N .

License: Creative Commons Attribution-Share Alike 3.0

translated.by crowd

Like this translation? Share it or bookmark!