класс ActiveRecord::Migration
Миграции Active Record
Миграции позволяют управлять развитием схемы, используемой несколькими физическими базами данных. Это решение распространённой проблемы: вы добавляете поле, необходимое для работы новой функции в локальной базе данных, но не знаете, как передать это изменение другим разработчикам и на рабочий сервер. С помощью миграций можно описать преобразования в автономных классах, которые можно поместить в систему контроля версий и выполнить для другой базы данных, отстающей на одну, две или пять версий.
Пример простой миграции:
class AddSsl < ActiveRecord::Migration[8.1]
def up
add_column :accounts, :ssl_enabled, :boolean, default: true
end
def down
remove_column :accounts, :ssl_enabled
end
end
Эта миграция добавит логический флаг в таблицу accounts и удалит его при откате миграции. Здесь показано, что все миграции имеют два метода up и down, описывающих преобразования, необходимые для применения или отмены миграции. Эти методы могут включать как специфичные для миграции методы, например add_column и remove_column, так и обычный код Ruby для создания данных, необходимых для преобразований.
Пример более сложной миграции, которой также требуется инициализировать данные:
class AddSystemSettings < ActiveRecord::Migration[8.1]
def up
create_table :system_settings do |t|
t.string :name
t.string :label
t.text :value
t.string :type
t.integer :position
end
SystemSetting.create name: 'notice',
label: 'Use notice?',
value: 1
end
def down
drop_table :system_settings
end
end
Сначала эта миграция добавляет таблицу system_settings, а затем создаёт в ней первую строку, используя модель Active Record, которая зависит от этой таблицы. Здесь также используется более продвинутый синтаксис create_table, позволяющий указать полную схему таблицы в одном блоке.
Доступные преобразования
Создание
-
create_join_table(table_1, table_2, options): Создаёт соединительную таблицу, имя которой составляется в лексикографическом порядке из первых двух аргументов. Подробнее см. в разделеActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table. -
create_table(name, options): Создаёт таблицу с именемnameи делает объект таблицы доступным в блоке, где можно добавлять в неё столбцы в том же формате, что и вadd_column. См. пример выше. Хеш параметров предназначен для фрагментов вроде «DEFAULT CHARSET=UTF-8», которые добавляются к определению создаваемой таблицы. -
add_column(table_name, column_name, type, options): Добавляет в таблицуtable_nameновый столбец с именемcolumn_name, тип которого должен быть одним из следующих::string,:text,:integer,:float,:decimal,:datetime,:timestamp,:time,:date,:binary,:boolean. Значение по умолчанию можно задать, передав хешoptions, например{ default: 11 }. Другие параметры включают:limitи:null(например,{ limit: 50, null: false }). Подробнее см. в разделеActiveRecord::ConnectionAdapters::TableDefinition#column. -
add_foreign_key(from_table, to_table, options): Добавляет внешний ключ.from_table— это таблица со столбцом-ключом, аto_tableсодержит ссылку на первичный ключ. -
add_index(table_name, column_names, options): Добавляет новый индекс с именем столбца. Другие параметры включают:name,:unique(например,{ name: 'users_name_index', unique: true }) и:order(например,{ order: { name: :desc } }). -
add_reference(:table_name, :reference_name): Добавляет новый столбецreference_name_id, имеющий по умолчанию тип integer. Подробнее см. в разделеActiveRecord::ConnectionAdapters::SchemaStatements#add_reference. -
add_timestamps(table_name, options): Добавляет вtable_nameстолбцы временных меток (created_atиupdated_at).
Изменение
-
change_column(table_name, column_name, type, options): Изменяет тип столбца, используя те же параметры, что и add_column. -
change_column_default(table_name, column_name, default_or_changes): Задаёт дляcolumn_nameзначение по умолчанию, определённое черезdefault_or_changes, в таблицеtable_name. Передача хеша, содержащего:fromи:to, в качествеdefault_or_changesпозволит отменить это изменение при откате миграции. -
change_column_null(table_name, column_name, null, default = nil): Устанавливает или удаляет ограничениеNOT NULLдляcolumn_name. Флагnullуказывает, может ли значение бытьNULL. Подробнее см. в разделеActiveRecord::ConnectionAdapters::SchemaStatements#change_column_null. -
change_table(name, options): Позволяет изменять столбцы таблицы с именемname. Объект таблицы становится доступен в блоке, где можно добавлять и удалять столбцы, индексы или внешние ключи. -
rename_column(table_name, column_name, new_column_name): Переименовывает столбец, сохраняя его тип и содержимое. -
rename_index(table_name, old_name, new_name): Переименовывает индекс. -
rename_table(old_name, new_name): Переименовывает таблицуold_nameвnew_name.
Удаление
-
drop_table(*names): Удаляет указанные таблицы. -
drop_join_table(table_1, table_2, options): Удаляет соединительную таблицу, заданную указанными аргументами. -
remove_column(table_name, column_name, type, options): Удаляет столбец с именемcolumn_nameиз таблицыtable_name. -
remove_columns(table_name, *column_names): Удаляет указанные столбцы из определения таблицы. -
remove_foreign_key(from_table, to_table = nil, **options): Удаляет указанный внешний ключ из таблицыtable_name. -
remove_index(table_name, column: column_names): Удаляет индекс, указанный параметромcolumn_names. -
remove_index(table_name, name: index_name): Удаляет индекс, указанный параметромindex_name. -
remove_reference(table_name, ref_name, options): Удаляет ссылки наtable_name, заданные параметромref_name. -
remove_timestamps(table_name, options): Удаляет столбцы временных меток (created_atиupdated_at) из определения таблицы.
Необратимые преобразования
Некоторые преобразования являются разрушительными и не могут быть отменены. Такие миграции должны вызывать исключение ActiveRecord::IrreversibleMigration в методе down.
Запуск миграций из Rails
В пакет Rails входят несколько инструментов для создания и применения миграций.
Чтобы создать новую миграцию, используйте
$ bin/rails generate migration MyNewMigration
где MyNewMigration — имя миграции. Генератор создаст пустой файл миграции timestamp_my_new_migration.rb в каталоге db/migrate/, где timestamp — дата и время создания миграции в формате UTC.
Для создания миграций, добавляющих поля в таблицу, предусмотрено специальное синтаксическое сокращение.
$ bin/rails generate migration add_fieldname_to_tablename fieldname:string
Эта команда создаст файл timestamp_add_fieldname_to_tablename.rb, который будет выглядеть так:
class AddFieldnameToTablename < ActiveRecord::Migration[8.1]
def change
add_column :tablenames, :fieldname, :string
end
end
Чтобы запустить миграции для текущей настроенной базы данных, используйте bin/rails db:migrate. Эта команда обновит базу данных, выполнив все ожидающие миграции, и создаст таблицу schema_migrations, если она отсутствует (см. раздел «О таблице schema_migrations» ниже). Кроме того, будет вызвана команда db:schema:dump, которая обновит файл db/schema.rb в соответствии со структурой базы данных.
Чтобы откатить базу данных к предыдущей версии миграции, используйте bin/rails db:rollback VERSION=X, где X — версия, до которой нужно выполнить откат. Также можно использовать параметр STEP, чтобы откатить несколько последних миграций. bin/rails db:rollback STEP=2 выполнит откат двух последних миграций.
Если какая-либо миграция вызовет исключение ActiveRecord::IrreversibleMigration, этот шаг завершится с ошибкой, и вам придётся выполнить некоторые действия вручную.
Другие примеры
Не все миграции изменяют схему. Некоторые лишь исправляют данные:
class RemoveEmptyTags < ActiveRecord::Migration[8.1]
def up
Tag.all.each { |tag| tag.destroy if tag.pages.empty? }
end
def down
# not much we can do to restore deleted data
raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
end
end
Другие при применении миграции удаляют столбцы, а не при её откате:
class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[8.1]
def up
remove_column :items, :incomplete_items_count
remove_column :items, :completed_items_count
end
def down
add_column :items, :incomplete_items_count
add_column :items, :completed_items_count
end
end
А иногда нужно выполнить SQL-команду, для которой в миграциях нет прямой абстракции:
class MakeJoinUnique < ActiveRecord::Migration[8.1]
def up
execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
end
def down
execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
end
end
Использование модели после изменения её таблицы
Иногда нужно добавить столбец в миграции и сразу же заполнить его. В этом случае необходимо вызвать Base#reset_column_information, чтобы убедиться, что модель получила актуальные сведения о столбцах после добавления нового столбца. Пример:
class AddPeopleSalary < ActiveRecord::Migration[8.1]
def up
add_column :people, :salary, :integer
Person.reset_column_information
Person.all.each do |p|
p.update_attribute :salary, SalaryCalculator.compute(p)
end
end
end
Управление подробностью вывода
По умолчанию миграции описывают выполняемые действия и выводят их в консоль по мере выполнения, а также показывают время, затраченное на каждый шаг.
Чтобы отключить этот вывод, задайте ActiveRecord::Migration.verbose = false.
Также можно выводить собственные сообщения и замеры времени с помощью метода say_with_time:
def up
...
say_with_time "Updating salaries..." do
Person.all.each do |p|
p.update_attribute :salary, SalaryCalculator.compute(p)
end
end
...
end После завершения блока будет выведена фраза «Обновление зарплат…» вместе с замером времени его выполнения.
Миграции с временными метками
По умолчанию Rails создаёт миграции следующего вида:
20080717013526_your_migration_name.rb
Префикс представляет собой временную метку создания (в формате UTC). Не следует изменять временные метки вручную. Чтобы проверить, что временные метки миграций соответствуют ожидаемому Active Record формату, можно использовать следующий параметр конфигурации:
config.active_record.validate_migration_timestamps = true
Если вы предпочитаете числовые префиксы, отключите миграции с временными метками, задав:
config.active_record.timestamped_migrations = false
В файле application.rb.
Обратимые миграции
Обратимые миграции умеют самостоятельно выполнять down. Достаточно указать логику up, и система Migration определит, как выполнить для вас команды отката.
Чтобы определить обратимую миграцию, добавьте в неё метод change, например:
class TenderloveMigration < ActiveRecord::Migration[8.1]
def change
create_table(:horses) do |t|
t.column :content, :text
t.column :remind_at, :datetime
end
end
end
При применении эта миграция создаст таблицу horses, а при откате автоматически определит, как удалить её.
Некоторые команды нельзя отменить. Если вы хотите определить действия для применения и отката в таких случаях, как и прежде, необходимо определить методы up и down.
Если команду нельзя отменить, при откате миграции будет вызвано исключение ActiveRecord::IrreversibleMigration.
Список обратимых команд см. в разделе ActiveRecord::Migration::CommandRecorder.
Транзакционные миграции
Если адаптер базы данных поддерживает транзакции DDL, все миграции автоматически будут выполняться в транзакции. Однако некоторые запросы нельзя выполнять внутри транзакции; в таких случаях можно отключить автоматические транзакции.
class ChangeEnum < ActiveRecord::Migration[8.1]
disable_ddl_transaction!
def up
execute "ALTER TYPE model_size ADD VALUE 'new_value'"
end
end
Помните, что вы по-прежнему можете открывать собственные транзакции, даже если находитесь в Migration с self.disable_ddl_transaction!.
Атрибуты
Публичные методы класса
# File activerecord/lib/active_record/migration.rb, line 629 def self.[](version) Compatibility.find(version) end
# File activerecord/lib/active_record/migration.rb, line 693
def check_all_pending!
pending_migrations = []
ActiveRecord::Tasks::DatabaseTasks.with_temporary_pool_for_each(env: env) do |pool|
if pending = pool.migration_context.open.pending_migrations
pending_migrations << pending
end
end
migrations = pending_migrations.flatten
if migrations.any?
raise ActiveRecord::PendingMigrationError.new(pending_migrations: migrations)
end
end Вызывает ошибку ActiveRecord::PendingMigrationError, если в окружении для любой конфигурации базы данных есть ожидающие миграции.
# File activerecord/lib/active_record/migration.rb, line 633 def self.current_version ActiveRecord::VERSION::STRING.to_f end
# File activerecord/lib/active_record/migration.rb, line 735 def disable_ddl_transaction! @disable_ddl_transaction = true end
Отключает обёртывание этой миграции в транзакцию. Даже после вызова disable_ddl_transaction! вы можете создавать собственные транзакции.
Подробнее см. в разделе «Транзакционные миграции» выше.
# File activerecord/lib/active_record/migration.rb, line 709
def load_schema_if_pending!
if any_schema_needs_update?
load_schema!
end
check_pending_migrations
end # File activerecord/lib/active_record/migration.rb, line 727 def migrate(direction) new.migrate direction end
# File activerecord/lib/active_record/migration.rb, line 805 def initialize(name = self.class.name, version = nil) @name = name @version = version @connection = nil @pool = nil end
# File activerecord/lib/active_record/migration.rb, line 802 cattr_accessor :verbose
Указывает, будут ли миграции выводить в консоль выполняемые действия по мере их выполнения, а также время, затраченное на каждый шаг. По умолчанию — true.
Публичные методы экземпляра
# File activerecord/lib/active_record/migration.rb, line 1010
def announce(message)
text = "#{version} #{name}: #{message}"
length = [0, 75 - text.length].max
write "== %s %s" % [text, "=" * length]
end # File activerecord/lib/active_record/migration.rb, line 1041 def connection @connection || ActiveRecord::Tasks::DatabaseTasks.migration_connection end
# File activerecord/lib/active_record/migration.rb, line 1045 def connection_pool @pool || ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool end
# File activerecord/lib/active_record/migration.rb, line 1066
def copy(destination, sources, options = {})
copied = []
FileUtils.mkdir_p(destination) unless File.exist?(destination)
schema_migration = SchemaMigration::NullSchemaMigration.new
internal_metadata = InternalMetadata::NullInternalMetadata.new
destination_migrations = ActiveRecord::MigrationContext.new(destination, schema_migration, internal_metadata).migrations
last = destination_migrations.last
sources.each do |scope, path|
source_migrations = ActiveRecord::MigrationContext.new(path, schema_migration, internal_metadata).migrations
source_migrations.each do |migration|
source = File.binread(migration.filename)
inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
magic_comments = +""
loop do
# If we have a magic comment in the original migration,
# insert our comment after the first newline(end of the magic comment line)
# so the magic keep working.
# Note that magic comments must be at the first line(except sh-bang).
source.sub!(/\A(?:#.*\b(?:en)?coding:\s*\S+|#\s*frozen_string_literal:\s*(?:true|false)).*\n/) do |magic_comment|
magic_comments << magic_comment; ""
end || break
end
if !magic_comments.empty? && source.start_with?("\n")
magic_comments << "\n"
source = source[1..-1]
end
source = "#{magic_comments}#{inserted_comment}#{source}"
if duplicate = destination_migrations.detect { |m| m.name == migration.name }
if options[:on_skip] && duplicate.scope != scope.to_s
options[:on_skip].call(scope, migration)
end
next
end
migration.version = next_migration_number(last ? last.version + 1 : 0).to_i
new_path = File.join(destination, "#{migration.version}_#{migration.name.underscore}.#{scope}.rb")
old_path, migration.filename = migration.filename, new_path
last = migration
File.binwrite(migration.filename, source)
copied << migration
options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
destination_migrations << migration
end
end
copied
end # File activerecord/lib/active_record/migration.rb, line 962 def down self.class.delegate = self return unless self.class.respond_to?(:down) self.class.down end
# File activerecord/lib/active_record/migration.rb, line 990
def exec_migration(conn, direction)
@connection = conn
if respond_to?(:change)
if direction == :down
revert { change }
else
change
end
else
public_send(direction)
end
ensure
@connection = nil
@execution_strategy = nil
end # File activerecord/lib/active_record/migration.rb, line 812 def execution_strategy @execution_strategy ||= ActiveRecord.migration_strategy.new(self) end
# File activerecord/lib/active_record/migration.rb, line 1049
def method_missing(method, *arguments, &block)
say_with_time "#{method}(#{format_arguments(arguments)})" do
unless connection.respond_to? :revert
unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
arguments[0] = proper_table_name(arguments.first, table_name_options)
if method == :rename_table ||
(method == :remove_foreign_key && !arguments.second.is_a?(Hash))
arguments[1] = proper_table_name(arguments.second, table_name_options)
end
end
end
return super unless execution_strategy.respond_to?(method)
execution_strategy.send(method, *arguments, &block)
end
end # File activerecord/lib/active_record/migration.rb, line 969
def migrate(direction)
return unless respond_to?(direction)
case direction
when :up then announce "migrating"
when :down then announce "reverting"
end
time_elapsed = nil
ActiveRecord::Tasks::DatabaseTasks.migration_connection.pool.with_connection do |conn|
time_elapsed = ActiveSupport::Benchmark.realtime do
exec_migration(conn, direction)
end
end
case direction
when :up then announce "migrated (%.4fs)" % time_elapsed; write
when :down then announce "reverted (%.4fs)" % time_elapsed; write
end
end Выполняет эту миграцию в указанном направлении
# File activerecord/lib/active_record/migration.rb, line 1133
def next_migration_number(number)
if ActiveRecord.timestamped_migrations
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
else
"%.3d" % number.to_i
end
end Определяет номер версии следующей миграции.
# File activerecord/lib/active_record/migration.rb, line 1124
def proper_table_name(name, options = {})
if name.respond_to? :table_name
name.table_name
else
"#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
end
end Находит правильное имя таблицы для объекта Active Record. Использует собственное имя table_name объекта Active Record либо префикс или суффикс из переданных параметров.
# File activerecord/lib/active_record/migration.rb, line 914
def reversible
helper = ReversibleBlockHelper.new(reverting?)
execute_block { yield helper }
end Используется для задания операции, которую можно выполнить в одном из двух направлений. Вызовите методы up и down переданного объекта, чтобы выполнить блок только в одном направлении. Весь блок будет вызван в правильном порядке в рамках миграции.
В следующем примере перебор пользователей всегда выполняется, когда существуют все три столбца: ‘first_name’, ‘last_name’ и ‘full_name’, даже при откате миграции:
class SplitNameMigration < ActiveRecord::Migration[8.1]
def change
add_column :users, :first_name, :string
add_column :users, :last_name, :string
reversible do |dir|
User.reset_column_information
User.all.each do |u|
dir.up { u.first_name, u.last_name = u.full_name.split(' ') }
dir.down { u.full_name = "#{u.first_name} #{u.last_name}" }
u.save
end
end
revert { add_column :users, :full_name, :string }
end
end
# File activerecord/lib/active_record/migration.rb, line 857
def revert(*migration_classes, &block)
run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
if block_given?
if connection.respond_to? :revert
connection.revert(&block)
else
recorder = command_recorder
@connection = recorder
suppress_messages do
connection.revert(&block)
end
@connection = recorder.delegate
recorder.replay(self)
end
end
end Отменяет команды миграции для указанного блока и указанных миграций.
При применении следующая миграция удалит таблицу ‘horses’ и создаст таблицу ‘apples’, а при откате выполнит обратные действия.
class FixTLMigration < ActiveRecord::Migration[8.1]
def change
revert do
create_table(:horses) do |t|
t.text :content
t.datetime :remind_at
end
end
create_table(:apples) do |t|
t.string :variety
end
end
end
Или, что эквивалентно, если TenderloveMigration определён, как описано в документации по Migration:
require_relative "20121212123456_tenderlove_migration"
class FixupTLMigration < ActiveRecord::Migration[8.1]
def change
revert TenderloveMigration
create_table(:apples) do |t|
t.string :variety
end
end
end
Эту команду можно вкладывать.
# File activerecord/lib/active_record/migration.rb, line 874 def reverting? connection.respond_to?(:reverting) && connection.reverting end
# File activerecord/lib/active_record/migration.rb, line 942
def run(*migration_classes)
opts = migration_classes.extract_options!
dir = opts[:direction] || :up
dir = (dir == :down ? :up : :down) if opts[:revert]
if reverting?
# If in revert and going :up, say, we want to execute :down without reverting, so
revert { run(*migration_classes, direction: dir, revert: true) }
else
migration_classes.each do |migration_class|
migration_class.new.exec_migration(connection, dir)
end
end
end Запускает указанные классы миграции. Последний аргумент может задавать параметры:
-
:direction— значение по умолчанию::up. -
:revert— значение по умолчанию:false.
# File activerecord/lib/active_record/migration.rb, line 1018
def say(message, subitem = false)
write "#{subitem ? " ->" : "--"} #{message}"
end Принимает сообщение и выводит его без изменений. Вторым аргументом можно передать логическое значение, указывающее, нужно ли добавлять отступ.
# File activerecord/lib/active_record/migration.rb, line 1024
def say_with_time(message)
say(message)
result = nil
time_elapsed = ActiveSupport::Benchmark.realtime { result = yield }
say "%.4fs" % time_elapsed, :subitem
say("#{result} rows", :subitem) if result.is_a?(Integer)
result
end Выводит текст и время, затраченное на выполнение блока. Если блок возвращает целое число, оно считается количеством затронутых строк.
# File activerecord/lib/active_record/migration.rb, line 1034 def suppress_messages save, self.verbose = verbose, false yield ensure self.verbose = save end
Принимает блок и подавляет все сообщения, выводимые этим блоком.
# File activerecord/lib/active_record/migration.rb, line 956 def up self.class.delegate = self return unless self.class.respond_to?(:up) self.class.up end
# File activerecord/lib/active_record/migration.rb, line 933 def up_only(&block) execute_block(&block) unless reverting? end
Используется для задания операции, выполняемой только при применении миграции (например, для заполнения нового столбца начальными значениями).
В следующем примере новому столбцу published будет присвоено значение true для всех существующих записей.
class AddPublishedToPosts < ActiveRecord::Migration[8.1]
def change
add_column :posts, :published, :boolean, default: false
up_only do
execute "update posts set published = 'true'"
end
end
end
# File activerecord/lib/active_record/migration.rb, line 1006 def write(text = "") puts(text) if verbose end
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.