Spec-Zone.ru › Ruby on Rails 5.2

класс ActiveRecord::Migration

Родитель:
Object

Миграции Active Record

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

Пример простой миграции:

class AddSsl < ActiveRecord::Migration[5.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[5.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(name): Удаляет таблицу под названием name.

  • 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, options_or_to_table): Удаляет указанный внешний ключ из таблицы под названием 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 имеет несколько инструментов для создания и применения миграций.

Для генерации новой миграции используйте

rails generate migration MyNewMigration

где MyNewMigration — имя вашей миграции. Генератор создаст пустой файл миграции timestamp_my_new_migration.rb в каталоге db/migrate/, где timestamp — отформатированная в UTC дата и время создания миграции.

Существует специальное синтаксическое сокращение для генерации миграций, добавляющих поля в таблицу.

rails generate migration add_fieldname_to_tablename fieldname:string

Это сгенерирует файл timestamp_add_fieldname_to_tablename.rb, который будет выглядеть так:

class AddFieldnameToTablename < ActiveRecord::Migration[5.0]
  def change
    add_column :tablenames, :fieldname, :string
  end
end

Для запуска миграций на текущей базе данных используйте rails db:migrate. Это обновит базу данных, выполнив все ожидающие миграции, создав таблицу schema_migrations (см. раздел «О таблице schema_migrations»), если она отсутствует. Также будет вызван тask db:schema:dump, который обновит файл db/schema.rb, чтобы он соответствовал структуре вашей базы данных.

Чтобы откатить базу данных до предыдущей версии миграции, используйте rails db:rollback VERSION=X , где X — версия, до которой вы хотите понизить. В качестве альтернативы, можно также использовать опцию STEP, если вы хотите откатить несколько последних миграций. rails db:rollback STEP=2 откатит две последние миграции.

Если какая-либо из миграций выбросит исключение ActiveRecord::IrreversibleMigration, этот шаг завершится с ошибкой, и вам потребуется ручная работа.

Поддержка баз данных

Миграции в настоящее время поддерживаются в MySQL, PostgreSQL, SQLite, SQL Server и Oracle (все поддерживаемые базы данных, кроме DB2).

Дополнительные примеры

Не все миграции изменяют схему. Некоторые просто исправляют данные:

class RemoveEmptyTags < ActiveRecord::Migration[5.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[5.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[5.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[5.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).

Если вы предпочитаете использовать числовые префиксы, вы можете отключить миграции со временем создания, установив:

config.active_record.timestamped_migrations = false

В файле application.rb.

Обратимые миграции

Обратимые миграции — это миграции, которые знают, как выполнить down за вас. Вам просто нужно указать логику up, а система миграций сама определит, как выполнить команды обратной миграции.

Для определения обратимой миграции определите метод change в вашей миграции следующим образом:

class TenderloveMigration < ActiveRecord::Migration[5.0]
  def change
    create_table(:horses) do |t|
      t.column :content, :text
      t.column :remind_at, :datetime
    end
  end
end

Эта миграция создаст таблицу лошадей для вас при повышении версии и автоматически определит, как удалить таблицу при понижении.

Некоторые команды, например, remove_column необратимы. Если вы хотите определить, как перемещаться вверх и вниз в этих случаях, вы должны определить методы up и down как и прежде.

Если команда необратима, при понижении версии миграции будет вызвано исключение ActiveRecord::IrreversibleMigration.

Список обратимых команд см. в ActiveRecord::Migration::CommandRecorder.

Транзакционные миграции

Если адаптер базы данных поддерживает транзакции DDL, все миграции будут автоматически обернуты в транзакцию. Однако есть запросы, которые нельзя выполнить внутри транзакции, и для таких ситуаций можно отключить автоматические транзакции.

class ChangeEnum < ActiveRecord::Migration[5.0]
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end

Помните, что вы всё ещё можете открывать свои собственные транзакции, даже если вы находитесь в миграции с self.disable_ddl_transaction!.

Атрибуты

name[RW]
version[RW]

Методы публичного класса

[](version) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 535
def self.[](version)
  Compatibility.find(version)
end
check_pending!(connection = Base.connection) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 578
def check_pending!(connection = Base.connection)
  raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration?
end

Вызывает ошибку ActiveRecord::PendingMigrationError если какие-либо миграции ожидают выполнения.

current_version() Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 539
def self.current_version
  ActiveRecord::VERSION::STRING.to_f
end
disable_ddl_transaction!() Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 615
def disable_ddl_transaction!
  @disable_ddl_transaction = true
end

Отключает обертывание транзакций для этой миграции. Вы всё ещё можете создавать свои собственные транзакции, даже после вызова disable_ddl_transaction!

Для получения более подробной информации прочитайте раздел “Транзакционные миграции” выше.

load_schema_if_pending!() Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 582
def load_schema_if_pending!
  if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations?
    # Roundtrip to Rake to allow plugins to hook into database initialization.
    root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root
    FileUtils.cd(root) do
      current_config = Base.connection_config
      Base.clear_all_connections!
      system("bin/rails db:test:prepare")
      # Establish a new connection, the old database may be gone (db:test:prepare uses purge)
      Base.establish_connection(current_config)
    end
    check_pending!
  end
end
migrate(direction) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 607
def migrate(direction)
  new.migrate direction
end
new(name = self.class.name, version = nil) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 627
def initialize(name = self.class.name, version = nil)
  @name       = name
  @version    = version
  @connection = nil
end

Методы публичного экземпляра

announce(message) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 827
def announce(message)
  text = "#{version} #{name}: #{message}"
  length = [0, 75 - text.length].max
  write "== %s %s" % [text, "=" * length]
end
connection() Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 853
def connection
  @connection || ActiveRecord::Base.connection
end
copy(destination, sources, options = {}) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 875
def copy(destination, sources, options = {})
  copied = []

  FileUtils.mkdir_p(destination) unless File.exist?(destination)

  destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations
  last = destination_migrations.last
  sources.each do |scope, path|
    source_migrations = ActiveRecord::MigrationContext.new(path).migrations

    source_migrations.each do |migration|
      source = File.binread(migration.filename)
      inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
      magic_comments = "".dup
      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
      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
down() Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 780
def down
  self.class.delegate = self
  return unless self.class.respond_to?(:down)
  self.class.down
end
exec_migration(conn, direction) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 808
def exec_migration(conn, direction)
  @connection = conn
  if respond_to?(:change)
    if direction == :down
      revert { change }
    else
      change
    end
  else
    send(direction)
  end
ensure
  @connection = nil
end
method_missing(method, *arguments, &block) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 857
def method_missing(method, *arguments, &block)
  arg_list = arguments.map(&:inspect) * ", "

  say_with_time "#{method}(#{arg_list})" 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 [:rename_table, :add_foreign_key].include?(method) ||
          (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 connection.respond_to?(method)
    connection.send(method, *arguments, &block)
  end
end
Вызывает метод суперкласса
migrate(direction) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 787
def migrate(direction)
  return unless respond_to?(direction)

  case direction
  when :up   then announce "migrating"
  when :down then announce "reverting"
  end

  time = nil
  ActiveRecord::Base.connection_pool.with_connection do |conn|
    time = Benchmark.measure do
      exec_migration(conn, direction)
    end
  end

  case direction
  when :up   then announce "migrated (%.4fs)" % time.real; write
  when :down then announce "reverted (%.4fs)" % time.real; write
  end
end

Выполняет данную миграцию в указанном направлении

next_migration_number(number) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 934
def next_migration_number(number)
  if ActiveRecord::Base.timestamped_migrations
    [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
  else
    SchemaMigration.normalize_migration_number(number)
  end
end

Определяет номер версии следующей миграции.

proper_table_name(name, options = {}) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 925
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 или префикс/суффикс из переданных опций.

reversible() { |helper| ... } Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 733
def reversible
  helper = ReversibleBlockHelper.new(reverting?)
  execute_block { yield helper }
end

Используется для задания операции, которая может выполняться в одном или другом направлении. Вызовите методы up и down объекта, полученного в результате вызова блока, для выполнения блока только в заданном направлении. Весь блок будет выполнен в правильном порядке в рамках миграции.

В следующем примере итерация по пользователям всегда будет выполнена, когда существуют три столбца 'first_name', 'last_name' и 'full_name', даже при миграции вниз:

class SplitNameMigration < ActiveRecord::Migration[5.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
revert(*migration_classes) { || ... } Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 674
def revert(*migration_classes)
  run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
  if block_given?
    if connection.respond_to? :revert
      connection.revert { yield }
    else
      recorder = CommandRecorder.new(connection)
      @connection = recorder
      suppress_messages do
        connection.revert { yield }
      end
      @connection = recorder.delegate
      recorder.commands.each do |cmd, args, block|
        send(cmd, *args, &block)
      end
    end
  end
end

Отменяет команды миграции для данного блока и заданных миграций.

Следующая миграция удалит таблицу 'horses' и создаст таблицу 'apples' при движении вверх, и наоборот при движении вниз.

class FixTLMigration < ActiveRecord::Migration[5.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[5.0]
  def change
    revert TenderloveMigration

    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

Эта команда может быть вложена.

reverting?() Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 693
def reverting?
  connection.respond_to?(:reverting) && connection.reverting
end
run(*migration_classes) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 760
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)

say(message, subitem = false) Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 833
def say(message, subitem = false)
  write "#{subitem ? "   ->" : "--"} #{message}"
end
say_with_time(message) { || ... } Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 837
def say_with_time(message)
  say(message)
  result = nil
  time = Benchmark.measure { result = yield }
  say "%.4fs" % time.real, :subitem
  say("#{result} rows", :subitem) if result.is_a?(Integer)
  result
end
suppress_messages() { || ... } Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 846
def suppress_messages
  save, self.verbose = verbose, false
  yield
ensure
  self.verbose = save
end
up() Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 774
def up
  self.class.delegate = self
  return unless self.class.respond_to?(:up)
  self.class.up
end
up_only() { || ... } Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 752
def up_only
  execute_block { yield } unless reverting?
end

Используется для задания операции, которая выполняется только при миграции вверх (например, заполнение нового столбца начальными значениями).

В следующем примере новому столбцу published будет присвоено значение true для всех существующих записей.

class AddPublishedToPosts < ActiveRecord::Migration[5.2]
  def change
    add_column :posts, :published, :boolean, default: false
    up_only do
      execute "update posts set published = 'true'"
    end
  end
end
write(text = "") Показать исходный код
# File activerecord/lib/active_record/migration.rb, line 823
def write(text = "")
  puts(text) if verbose
end

© 2004–2018 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API