Rails миграции - факты, которых вы, возможно не знали
Что такое миграции и зачем они нужны?
Эта статья рассказывает о том, что такое миграции баз данных, и, о некоторых возможностях в Rails, о которых не все знают. Если Вы ищете конкретное решение, то Вам будет проще найти его в моем сборнике рецептов
Миграции - это простой механизм управления структурой базы данный и данными в ней. Используя данный механизм программисты смогут легко синхронизиорвать структуру базы данных на локальных машинах и продакшен серверах.
Вместо того, чтобы изменять структуру и данные в базе данных руками, программист описывает миграцию - как некий процесс изменения текущего состояния базы к требуемому, Для этого он должен описать шаг UP
На псевдо языке примерно так
UP { СОЗДАТЬ ТАБЛИЦУ “пользователи” ДОБАВИТЬ КОЛОНКУ К “пользователи” С ИМЕНЕМ “имя” И ТИПОМ “строка” ДОБАВИТЬ КОЛОНКУ К “проекты” С ИМЕНЕМ “инедтификатор_пользователя” И ТИПОМ “число” }
и, тогда все другие программисты, получив эту команду, смогут выполнить ее у себя, тем самым изменив свою базу соответсвующим образом.
Но, вполне возможно, что программист ошибся описывая свои изменения, либо от них по каким-то причинам было решено отказаться, Для этого, программист вместе с методом UP должен описать метод DOWN, которые позволит откатить изменения до первоначального состояния
DOWN { УДАЛИТЬ ТАБЛИЦУ “пользователи” УДАЛИТЬ КОЛОНКУ “идентификатор пользователя” В ТАБЛИЦЕ “проекты” }
Теперь наши данные будут целостными в любом случае
Миграции в Rails
Для создания новых миграций в rails есть встроенный генератор
$ rails generate migration AddFirstAndLastNameToUsers
invoke active_record
create db/migrate/20150713171627_add_first_and_last_name_to_users.rb
Он сгенерирует новый файл в папке #{APP_ROOT}db/migrate/
class AddFirstAndLastNameToUsers < ActiveRecord::Migration
def up
add_column :users, :first_name, :string
add_column :users, :last_name, :string
end
def down
remove_column :users, :first_name
remove_column :users, :last_name
end
end
Так выглядели миграции в Rails 2
Начиная с Rails 3 миграции стали более умными. Стало очевидно, что описание способа миграции описывает и способ роллбека. Например, очевидно, что add_column :users, :first_name, :string
откатывается командой remove_column :users, :first_name
. create_table :projects
- drop_table :projects
и т.д;
Но change_column :users, :email, :text
- не дает информации для отката. Она говорит, что поле :email должно стать текстом, но не дает никакой информации о типе поля, который был раньше. И, в таких случаях нам по прежнему были нужны методы up и down
Используем change вместо up и down
class AddFirstAndLastNameToUsers < ActiveRecord::Migration
def change
add_column :users, :first_name, :string
add_column :users, :last_name, :string
end
end
Небольшой справочный материал
Доступные методы для изменения структуры данных
create_table(name, options):
Создать таблицу с именем name и в больщинстве случаев в качестве параметра принимает блок, который принимает объект таблицы и позволяет управлять структурой таблицы.Подробнее в официальной документации. В качестве опций может принимать следующие параметры::id
- По умолчанию миграция автоматиччески добавит в таблицу primary_key c именем :id. Чтобы это отключить передаем опцию id: false:primary_key
- имя первичного ключа если id: false. ActiveRecord автоматически подхватит новый ключ:temporary
- сделать таблицу временной:force
- По умолчанию false. При значении true удалит перед созданием таблицу с таким же именем. При значении :cascade удалит также все связанные таблицы:as
- позволяет указать SQL запрос для создания таблицы, При этом блок, опции :id и :primary_key игнорируются/span>
drop_table(name):
Удалит таблицу с именем namechange_table(name, options):
- Позволяет изменить таблицу с именем name, при этом синтаксис такой же как и с create_table.rename_table(old_name, new_name):
Переименование таблицыadd_column(table_name, column_name, type, options):
- Добавление колонки. В качестве опций может принимать следующие параметры::limit
- максимальная длина строки для типов :string && :text и количество байтов для :binary и :integer:default
- Значение по умолчанию.:null
- null: false наложит NOT NULL ограничение на колонку:precision
- максимальное число цифр в типа :decimal:scale
- количество цифр после запятой в типе :decimal
rename_column(table_name, column_name, new_column_name):
- переименование столбцаchange_column(table_name, column_name, type, options):
Изменение столбца - те же опции что и для добавления в зависимости от типа столбцаremove_column(table_name, column_name, type, options):
Удаление столбца - те же опции что и для добавления в зависимости от типа столбцadd_index(table_name, column_names, options):
- Добавление индекса, примеры можно найти в официальной документацииremove_index(table_name, column: column_name):
- Удаляет индекс
На самом деле таких методов намного больше, я привел только самые часто используемые. Полный перечень всех методов можно найти здесь
Rails 4 принес нам Reversible
$ rails generate migration ReplaceFirstNameAndLAstNameWIthName
invoke active_record
create db/migrate/20150713174412_replace_first_name_and_l_ast_name_w_ith_name.rb
Создаем новую миграцию. Ее целью будет замена в таблице :users полей last_name и first_name на одно поле name и сохранение существующих данных
class ReplaceFirstNameAndLAstNameWIthName < ActiveRecord::Migration
def change
add_column :users, :name, :string
reversible do |direction|
User.reset_column_information
User.all.each do |user|
direction.up { user.name= "#{user.first_name} #{user.last_name}" }
direction.down { user.first_name, user.last_name = user.name.split(' ') }
user.save
end
end
revert do
add_column :users, :first_name, :string
add_column :users, :last_name, :string
end
end
end
Давайте разберем подробнее, что здесь происходит. Метод revert принимает параметром блок и позволяет описывать часть миграции с обращаемой стороны, Т.е.
revert do
add_column :users, :first_name, :string
add_column :users, :last_name, :string
end
#эквавалентно
remove_column_users, :first_name
remove_column_users, :last_name
Но, во втором случае было бы невозможно выполнять миграцию назад.
Так же важно понимать, как работает сам механизм миграции, Когда rails выполняют команду add_column :users, :name, :string
- они физически ничего в базе не изменяют, Они составляют очередь изменений, которые необходимо выполнить и запускают их в нужном порядке, Т.е. при проходе UP выполнится add_column, reversible, revert, при проходе DOWN - revert, reversible, remove_column
Миграции больших объемов данных
Довольно часто нам нужно изменить структуру данных не потеряв данные.Все методы и средства Rails прямо из миграции, но, при большом объеме данных такая миграция может занимать не приемлемое количество времени. В таком случае лучше использовать функии базы данных для ускорения этой обработки, Пример с генерацией имени пользователя. Миграцию вперед мы заменили на быструю миграцию использюя функцию concat PostgreSQL
class ReplaceFirstNameAndLAstNameWIthName < ActiveRecord::Migration
def change
add_column :users, :name, :string
reversible do |direction|
direction.up do
say_with_time ‘Generating names’ do
execute "UPDATE users SET name = concat(first_name, ' ', last_name)"
end
end
direction.down do
User.reset_column_information
User.all.each do |user|
user.first_name, user.last_name = user.name.split(' ')
user.save
end
end
end
revert do
add_column :users, :first_name, :string
add_column :users, :last_name, :string
end
end
end
Также мы использовали метод say_with_time для отображения времени выполнения длянных операций
Выводы
Миграции отличный механизм для поддержания целостности структуры данных. Они легко позволяют работать в команде и изменять базу данных на сервере. Краткие заметки
- Для создания новой миграции можно использовать генератор rails g migration MigrationName
- Для запуска миграции rake db:migrate
- Для отката миграции rake db:rollback
- По возможности, все миграции запускаются в транзакции и все изменения будут отменены, если миграции не удалось успешно завершиться
- Всегда используйте default опцию колонки, вместо установки значения перед валидацие в модели.
- Для миграции данных лучше использовать чистый SQL
- Чтобы сделать SQL немного читаемей можно положить его части в массив, а потом соединить их в один запрос
execute [
'UPDATE channel_meta AS c_m ',
'SET inappropriate = true',
'WHERE',
'EXISTS(',
'SELECT channels.id AS channel_id, c.user_id AS user_id',
'FROM channels',
"INNER JOIN complaints AS c ON c.complaintable_id = channels.id AND c.complaintable_type = 'Channel'",
'WHERE c_m.channel_id = channels.id AND c_m.user_id = c.user_id)'
].join(' ')
Больше информации можно найти в API документации, а также в официальных гайдах на русском и на английском
Комментарии