跳至内容 跳至搜索

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` 的格式添加列。请参阅上面的示例。options 哈希用于“DEFAULT CHARSET=UTF-8”等片段,这些片段将附加到 create table 定义的末尾。

  • add_column(table_name, column_name, type, options):向名为 `table_name` 的表中添加一个名为 `column_name` 的新列,指定其类型为以下之一::string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean。可以通过传递一个像 { default: 11 } 这样的 options 哈希来指定默认值。其他选项包括 :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):向 table_name 添加时间戳(created_atupdated_at)列。

修改

  • change_column(table_name, column_name, type, options):使用与 add_column 相同的参数将列更改为不同的类型。

  • change_column_default(table_name, column_name, default_or_changes):为 table_name 上定义的 column_name 设置默认值 default_or_changes。将包含 :from:to 的哈希作为 default_or_changes 传递将使此更改在迁移中可逆。

  • change_column_null(table_name, column_name, null, default = nil):为 column_name 设置或删除 NOT NULL 约束。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):从名为 table_name 的表中删除名为 column_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_atupdated_at)。

不可逆转换

某些转换具有破坏性,无法逆转。这类迁移应在其 down 方法中引发 ActiveRecord::IrreversibleMigration 异常。

在 Rails 中运行迁移

Rails 包提供了多种工具来帮助创建和应用迁移。

要生成新的迁移,您可以使用

$ bin/rails generate migration MyNewMigration

其中 `MyNewMigration` 是您的迁移名称。生成器将在 `db/migrate/` 目录中创建一个空的迁移文件 `timestamp_my_new_migration.rb`,其中 `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

然后,“Updating salaries…”这句话就会被打印出来,并在块完成后显示其基准测试结果。

带时间戳的迁移

默认情况下,Rails 会生成如下所示的迁移:

20080717013526_your_migration_name.rb

前缀是生成时间戳(UTC)。不应手动修改时间戳。要验证迁移时间戳是否符合 Active Record 期望的格式,可以使用以下配置选项:

config.active_record.validate_migration_timestamps = true

如果您希望使用数字前缀,可以通过在 `application.rb` 中设置以下选项来关闭带时间戳的迁移:

config.active_record.timestamped_migrations = false

在 application.rb 中。

可逆迁移

可逆迁移是可以自动执行 `down` 操作的迁移。您只需提供 `up` 逻辑,Migration 系统就会自动为您执行 `down` 命令。

要定义一个可逆迁移,请在您的迁移中定义 `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

请记住,即使在具有 `self.disable_ddl_transaction!` 的 Migration 中,您仍然可以打开自己的事务。

命名空间
方法
#
A
C
D
E
L
M
N
P
R
S
U
V
W

Attributes

[RW] name
[RW] version

类公共方法

[](version)

# File activerecord/lib/active_record/migration.rb, line 629
def self.[](version)
  Compatibility.find(version)
end

check_all_pending!()

如果环境中所有数据库配置都有待处理的迁移,则会引发 ActiveRecord::PendingMigrationError 错误。

# 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

current_version()

# File activerecord/lib/active_record/migration.rb, line 633
def self.current_version
  ActiveRecord::VERSION::STRING.to_f
end

disable_ddl_transaction!()

禁用围绕此迁移的事务。即使在调用 disable_ddl_transaction! 后,您仍然可以创建自己的事务。

有关更多详细信息,请阅读上面的“事务性迁移”部分。

# File activerecord/lib/active_record/migration.rb, line 735
def disable_ddl_transaction!
  @disable_ddl_transaction = true
end

load_schema_if_pending!()

# 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

migrate(direction)

# File activerecord/lib/active_record/migration.rb, line 727
def migrate(direction)
  new.migrate direction
end

new(name = self.class.name, version = nil)

# 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

verbose

指定迁移是否将它们正在执行的操作写入控制台,同时附带描述每个步骤所花费时间的基准测试。默认为 true。

# File activerecord/lib/active_record/migration.rb, line 802
cattr_accessor :verbose

实例公共方法

announce(message)

# 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

connection()

# File activerecord/lib/active_record/migration.rb, line 1041
def connection
  @connection || ActiveRecord::Tasks::DatabaseTasks.migration_connection
end

connection_pool()

# File activerecord/lib/active_record/migration.rb, line 1045
def connection_pool
  @pool || ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool
end

copy(destination, sources, options = {})

# 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

down()

# 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

exec_migration(conn, direction)

# 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

execution_strategy()

# File activerecord/lib/active_record/migration.rb, line 812
def execution_strategy
  @execution_strategy ||= ActiveRecord.migration_strategy.new(self)
end

method_missing(method, *arguments, &block)

# 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

migrate(direction)

在指定方向执行此迁移

# 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

next_migration_number(number)

确定下一个迁移的版本号。

# 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

proper_table_name(name, options = {})

根据 Active Record 对象查找正确的表名。使用 Active Record 对象自己的 `table_name`,或使用传入选项中的前缀/后缀。

# 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

reversible()

用于指定一个可以双向执行的操作。调用所产生的对象的 `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 914
def reversible
  helper = ReversibleBlockHelper.new(reverting?)
  execute_block { yield helper }
end

revert(*migration_classes, &block)

反转给定块和给定迁移的迁移命令。

以下迁移将在向上迁移时删除“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 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

reverting?()

# File activerecord/lib/active_record/migration.rb, line 874
def reverting?
  connection.respond_to?(:reverting) && connection.reverting
end

run(*migration_classes)

运行给定的迁移类。最后一个参数可以指定选项:

  • :direction - 默认为 :up

  • :revert - 默认为 false

# 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

say(message, subitem = false)

接受一个消息参数并按原样输出。可以传递第二个布尔参数来指定是否缩进。

# File activerecord/lib/active_record/migration.rb, line 1018
def say(message, subitem = false)
  write "#{subitem ? "   ->" : "--"} #{message}"
end

say_with_time(message)

输出文本以及运行其块所花费的时间。如果块返回一个整数,它将假定为受影响的行数。

# 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

suppress_messages()

接受一个块作为参数,并抑制该块生成的任何输出。

# File activerecord/lib/active_record/migration.rb, line 1034
def suppress_messages
  save, self.verbose = verbose, false
  yield
ensure
  self.verbose = save
end

up()

# 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

up_only(&block)

用于指定一个仅在向上迁移时运行的操作(例如,用初始值填充新列)。

在下面的示例中,对于所有现有记录,新列 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 933
def up_only(&block)
  execute_block(&block) unless reverting?
end

write(text = "")

# File activerecord/lib/active_record/migration.rb, line 1006
def write(text = "")
  puts(text) if verbose
end