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)
: 向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)
: 为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)
: 删除table_name
上由ref_name
指定的引用。 -
remove_timestamps(table_name, options)
: 从表定义中删除时间戳列(created_at
和updated_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
然后将打印短语“Updating salaries…”,以及该块完成后该块的基准。
带时间戳的迁移
默认情况下,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 表,并在向下迁移时自动找出如何删除该表。
某些命令无法反转。如果您想定义这些情况下如何向上和向下移动,则应该像以前一样定义 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
请记住,即使您在使用 self.disable_ddl_transaction!
的 Migration
中,您仍然可以打开自己的事务。
- 模块 ActiveRecord::Migration::Compatibility
- 类 ActiveRecord::Migration::CheckPending
- 类 ActiveRecord::Migration::CommandRecorder
- #
- A
- C
- D
- E
- L
- M
- N
- P
- R
- S
- U
- V
- W
属性
[RW] | name | |
[RW] | version |
类公共方法
[](version) 链接
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 629 def self.[](version) Compatibility.find(version) end
check_all_pending!() 链接
如果任何迁移在环境中的所有数据库配置中处于挂起状态,则会引发 ActiveRecord::PendingMigrationError 错误。
源代码: 显示 | 在 GitHub 上
# 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() 链接
源代码: 显示 | 在 GitHub 上
# 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! 后,您仍然可以创建自己的事务。
有关更多详细信息,请阅读“上面“事务迁移”部分”。
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 735 def disable_ddl_transaction! @disable_ddl_transaction = true end
load_schema_if_pending!() 链接
源代码: 显示 | 在 GitHub 上
# 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) 链接
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 727 def migrate(direction) new.migrate direction end
new(name = self.class.name, version = nil) 链接
源代码: 显示 | 在 GitHub 上
# 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。
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 797 cattr_accessor :verbose
实例公共方法
announce(message) 链接
源代码: 显示 | 在 GitHub 上
# 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() 链接
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1036 def connection @connection || ActiveRecord::Tasks::DatabaseTasks.migration_connection end
connection_pool() 链接
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1040 def connection_pool @pool || ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool end
copy(destination, sources, options = {}) 链接
源代码: 显示 | 在 GitHub 上
# 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() 链接
源代码: 显示 | 在 GitHub 上
# 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) 链接
源代码: 显示 | 在 GitHub 上
# 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() 链接
源代码: 显示 | 在 GitHub 上
# 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) 链接
源代码: 显示 | 在 GitHub 上
# 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) 链接
在指定方向执行此迁移
源代码: 显示 | 在 GitHub 上
# 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) 链接
确定下一个迁移的版本号。
源代码: 显示 | 在 GitHub 上
# 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,或传入选项中的前缀/后缀。
源代码: 显示 | 在 GitHub 上
# 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() 链接
用于指定可以在一个方向或另一个方向运行的操作。调用 yield 对象的 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
源代码: 显示 | 在 GitHub 上
# 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
定义如迁移文档中所述
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
此命令可以嵌套。
源代码: 显示 | 在 GitHub 上
# 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?() 链接
源代码: 显示 | 在 GitHub 上
# 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
。
源代码: 显示 | 在 GitHub 上
# 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) 链接
接受一个消息参数并原样输出。可以传递第二个布尔参数以指定是否缩进。
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1013 def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end
say_with_time(message) 链接
输出文本以及运行其块所花费的时间。如果块返回一个整数,则假设它是受影响的行数。
来源:显示 | 在 GitHub 上
# 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() 链接
接受一个块作为参数,并抑制块生成的任何输出。
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1029 def suppress_messages save, self.verbose = verbose, false yield ensure self.verbose = save end
up() 链接
来源:显示 | 在 GitHub 上
# 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
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 928 def up_only(&block) execute_block(&block) unless reverting? end
write(text = "") 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1001 def write(text = "") puts(text) if verbose end