跳至內容 跳至搜尋

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 表格添加一個布林值旗標,如果您要撤銷遷移,則將其移除。它顯示了所有遷移都有兩個方法 updown,用於描述實施或移除遷移所需的轉換。這些方法可以包含遷移特定的方法,例如 add_columnremove_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 相同的格式。請參閱上面的範例。 options 散列用於附加到建立表格定義的片段,例如「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_atupdated_at)欄位新增到 table_name

修改

  • change_column(table_name, column_name, type, options):使用與 add_column 相同的參數將欄位更改為不同的類型。

  • change_column_default(table_name, column_name, default_or_changes):在 table_name 上設定由 default_or_changes 定義的 column_name 的預設值。傳遞包含 :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):移除由 ref_name 指定的 table_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.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 系統就會找出如何為您執行 down 命令。

要定義可逆遷移,請在遷移中定義 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

此遷移將在向上遷移時為您建立 horses 表格,並自動找出如何在向下遷移時刪除表格。

某些命令無法逆轉。如果您希望定義如何在這些情況下向上和向下移動,則應像以前一樣定義 updown 方法。

如果命令無法逆轉,則在遷移向下移動時將引發 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

請記住,即使您在使用 self.disable_ddl_transaction!Migration 中,您仍然可以開啟自己的交易。

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

屬性(Attributes)

[讀寫]([RW]) 名稱(name)
[讀寫]([RW]) 版本(version)

類別公開方法(Class Public methods)

[](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! 之後,您仍然可以建立自己的事務。

更多詳細資訊,請閱讀上方「事務性遷移(Transactional Migrations)」章節。

# 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 800
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 797
cattr_accessor :verbose

實例公開方法(Instance Public methods)

announce(message)

# 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

connection()

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

connection_pool()

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

copy(destination, sources, options = {})

# 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

down()

# 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

exec_migration(conn, direction)

# 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

execution_strategy()

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

method_missing(method, *arguments, &block)

# 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

migrate(direction)

在指定的方向執行此遷移

# 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

next_migration_number(number)

決定下一個遷移的版本號碼。

# 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

proper_table_name(name, options = {})

給定一個 Active Record 物件,找出正確的表格名稱。使用 Active Record 物件本身的 table_name,或傳入選項中的前綴/後綴。

# 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

reversible()

用於指定可以在一個方向或另一個方向執行的操作。呼叫傳入物件的 `up` 和 `down` 方法,僅在一個給定方向執行程式碼區塊。整個程式碼區塊將在遷移中以正確的順序呼叫。

在以下範例中,即使向下遷移,在 `first_name`、`last_name` 和 `full_name` 三個欄位存在時,始終會執行 users 的迴圈。

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 909
def reversible
  helper = ReversibleBlockHelper.new(reverting?)
  execute_block { yield helper }
end

revert(*migration_classes, &block)

反轉給定程式碼區塊和給定遷移的遷移指令。

以下遷移將在向上遷移時移除 `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 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

reverting?()

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

run(*migration_classes)

執行給定的遷移類別。最後一個參數可以指定選項

  • `:direction` - 預設值為 `:up`。

  • `:revert` - 預設值為 `false`。

# 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

say(message, subitem = false)

接受一個訊息參數並按原樣輸出。可以傳遞第二個布林值參數來指定是否縮排。

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

say_with_time(message)

輸出文字以及執行其程式碼區塊所花費的時間。如果程式碼區塊傳回一個整數,則假定它是受影響的列數。

# 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

suppress_messages()

接受一個程式碼區塊作為參數,並抑制程式碼區塊產生的任何輸出。

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

up()

# 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

up_only(&block)

用於指定僅在向上遷移時執行的操作(例如,使用初始值填充新欄位)。

在以下範例中,新欄位 `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 928
def up_only(&block)
  execute_block(&block) unless reverting?
end

write(text = "")

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