класс ActiveRecord::Migration
Миграции Active Record
Миграции могут управлять эволюцией схемы, используемой несколькими физическими базами данных. Это решение распространённой проблемы добавления поля для работы новой функции в локальной базе данных, но при этом неясно, как внести это изменение для других разработчиков и сервера в производстве. С помощью миграций вы можете описать преобразования в самостоятельных классах, которые можно включить в систему управления версиями и выполнить в другой базе данных, которая может быть на одну, две или пять версий позади.
Пример простой миграции:
class AddSsl < ActiveRecord::Migration[8.0]
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.0]
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(по умолчанию целого типа). Подробнее см.ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference. -
add_timestamps(table_name, options): Добавляет столбцы временных меток (created_atиupdated_at) в таблицуtable_name.
Изменение
-
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
def change
create_table(:horses) do |t|
t.column :content, :text
t.column :remind_at, :datetime
end
end
end
Эта миграция создаст таблицу лошадей для вас при повышении версии и автоматически определит, как удалить таблицу при понижении версии.
Некоторые команды нельзя отменить. Если вам нужно определить, как выполнить действия при повышении и понижении версии в этих случаях, определите методы up и down как и раньше.
Если команда необратима, при понижении версии миграции будет возбуждено исключение ActiveRecord::IrreversibleMigration.
Список обратимых команд см. в ActiveRecord::Migration::CommandRecorder.
Транзакционные миграции
Если адаптер базы данных поддерживает транзакции DDL, все миграции будут автоматически заключены в транзакцию. Однако есть запросы, которые нельзя выполнить внутри транзакции, и в таких ситуациях вы можете отключить автоматические транзакции.
class ChangeEnum < ActiveRecord::Migration[8.0]
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 800 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 797 cattr_accessor :verbose
Указывает, будут ли миграции выводить выполняемые действия в консоль по мере их выполнения, а также бенчмарки, описывающие время выполнения каждого шага. По умолчанию значение true.
Методы публичного экземпляра
# File activerecord/lib/active_record/migration.rb, line 1005
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 1036 def connection @connection || ActiveRecord::Tasks::DatabaseTasks.migration_connection end
# File activerecord/lib/active_record/migration.rb, line 1040 def connection_pool @pool || ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool end
# File activerecord/lib/active_record/migration.rb, line 1061
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 957 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 985
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 807 def execution_strategy @execution_strategy ||= ActiveRecord.migration_strategy.new(self) end
# File activerecord/lib/active_record/migration.rb, line 1044
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 964
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 1128
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 1119
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. Использует собственное имя таблицы объекта Active Record или префикс/суффикс из переданных параметров.
# File activerecord/lib/active_record/migration.rb, line 909
def reversible
helper = ReversibleBlockHelper.new(reverting?)
execute_block { yield helper }
end Используется для указания операции, которая может быть выполнена в одном или другом направлении. Вызовите методы up и down объекта, переданного в блоке, чтобы выполнить блок только в одном заданном направлении. Весь блок будет вызван в правильном порядке в пределах миграции.
В следующем примере цикл по пользователям всегда будет выполняться, когда существуют три столбца «first_name», «last_name» и «full_name», даже при миграции вниз:
class SplitNameMigration < ActiveRecord::Migration[8.0]
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 852
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.0]
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.0]
def change
revert TenderloveMigration
create_table(:apples) do |t|
t.string :variety
end
end
end
Эта команда может быть вложена.
# File activerecord/lib/active_record/migration.rb, line 869 def reverting? connection.respond_to?(:reverting) && connection.reverting end
# File activerecord/lib/active_record/migration.rb, line 937
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 1013
def say(message, subitem = false)
write "#{subitem ? " ->" : "--"} #{message}"
end Принимает аргумент сообщения и выводит его как есть. Второй булевый аргумент может быть передан для указания отступа или его отсутствия.
# File activerecord/lib/active_record/migration.rb, line 1019
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 1029 def suppress_messages save, self.verbose = verbose, false yield ensure self.verbose = save end
Принимает блок в качестве аргумента и подавляет вывод, генерируемый блоком.
# File activerecord/lib/active_record/migration.rb, line 951 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 928 def up_only(&block) execute_block(&block) unless reverting? end
Используется для указания операции, которая выполняется только при миграции вверх (например, заполнение нового столбца его начальными значениями).
В следующем примере новый столбец published получит значение true для всех существующих записей.
class AddPublishedToPosts < ActiveRecord::Migration[8.0]
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 1001 def write(text = "") puts(text) if verbose end
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.