活动记录迁移
迁移可以管理多个物理数据库使用的架构的演变。这是解决常见问题的解决方案,即在本地数据库中添加一个字段以使新功能正常工作,但不知道如何将该更改推送到其他开发人员和生产服务器。使用迁移,您可以在自包含类中描述转换,这些类可以检入版本控制系统,并针对可能落后一、二或五个版本的另一个数据库执行。
简单迁移示例
class AddSsl < ActiveRecord::Migration[7.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[7.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
表,然后使用依赖于该表的活动记录模型在其中创建第一行。它还使用了更高级的 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
。可以通过传递一个选项哈希(如{ 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
上为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(name)
:删除名为name
的表。 -
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[7.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[7.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[7.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[7.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[7.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
然后将打印短语“更新工资……”,以及块完成时的块基准。
带时间戳的迁移
默认情况下,Rails 生成的迁移看起来像
20080717013526_your_migration_name.rb
前缀是生成时间戳(UTC)。
如果你更喜欢使用数字前缀,可以通过设置关闭带时间戳的迁移
config.active_record.timestamped_migrations = false
在 application.rb 中。
可逆迁移
可逆迁移是知道如何为你执行 down
的迁移。你只需提供 up
逻辑,Migration
系统就会找出如何为你执行 down 命令。
要定义可逆迁移,请在迁移中像这样定义 change
方法
class TenderloveMigration < ActiveRecord::Migration[7.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[7.1]
disable_ddl_transaction!
def up
execute "ALTER TYPE model_size ADD VALUE 'new_value'"
end
end
请记住,即使您在 Migration
中使用 self.disable_ddl_transaction!
,您仍然可以打开自己的事务。
- MODULE ActiveRecord::Migration::Compatibility
- CLASS ActiveRecord::Migration::CheckPending
- CLASS ActiveRecord::Migration::CommandRecorder
- #
- A
- C
- D
- E
- L
- M
- N
- P
- R
- S
- U
- W
属性
[RW] | name | |
[RW] | version |
类公共方法
[](version) 链接
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 613 def self.[](version) Compatibility.find(version) end
check_all_pending!() 链接
如果在环境中的所有数据库配置中任何迁移都处于待处理状态,则引发 ActiveRecord::PendingMigrationError 错误。
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 698 def check_all_pending! pending_migrations = [] ActiveRecord::Tasks::DatabaseTasks.with_temporary_connection_for_each(env: env) do |connection| if pending = connection.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
check_pending!(connection = ActiveRecord::Tasks::DatabaseTasks.migration_connection) 链接
如果任何迁移处于待处理状态,则引发 ActiveRecord::PendingMigrationError 错误。
此方法已弃用,建议使用 check_all_pending!
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 682 def check_pending!(connection = ActiveRecord::Tasks::DatabaseTasks.migration_connection) ActiveRecord.deprecator.warn(<<-MSG.squish) The `check_pending!` method is deprecated in favor of `check_all_pending!`. The new implementation will loop through all available database configurations and find pending migrations. The prior implementation did not permit this. MSG pending_migrations = connection.migration_context.open.pending_migrations if pending_migrations.any? raise ActiveRecord::PendingMigrationError.new(pending_migrations: pending_migrations) end end
current_version() 链接
源代码: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 617 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 747 def disable_ddl_transaction! @disable_ddl_transaction = true end
load_schema_if_pending!() 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 714 def load_schema_if_pending! if any_schema_needs_update? # Roundtrip to Rake to allow plugins to hook into database initialization. root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root FileUtils.cd(root) do Base.connection_handler.clear_all_connections!(:all) system("bin/rails db:test:prepare") end end check_pending_migrations end
migrate(direction) 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 739 def migrate(direction) new.migrate direction end
new(name = self.class.name, version = nil) 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 796 def initialize(name = self.class.name, version = nil) @name = name @version = version @connection = nil end
实例公共方法
announce(message) 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1000 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 1031 def connection @connection || ActiveRecord::Tasks::DatabaseTasks.migration_connection end
copy(destination, sources, options = {}) 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1052 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 952 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 980 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 802 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 1035 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 959 def migrate(direction) return unless respond_to?(direction) case direction when :up then announce "migrating" when :down then announce "reverting" end time = nil ActiveRecord::Tasks::DatabaseTasks.migration_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) 链接
确定下一个迁移的版本号。
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1119 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 1110 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[7.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
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 904 def reversible helper = ReversibleBlockHelper.new(reverting?) execute_block { yield helper } end
revert(*migration_classes, &block) 链接
反转给定块和给定迁移的迁移命令。
以下迁移将在上行途中删除表“horses”,并创建表“apples”,在下行途中反向执行。
class FixTLMigration < ActiveRecord::Migration[7.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
的定义如迁移文档所述
require_relative "20121212123456_tenderlove_migration"
class FixupTLMigration < ActiveRecord::Migration[7.1]
def change
revert TenderloveMigration
create_table(:apples) do |t|
t.string :variety
end
end
end
此命令可以嵌套。
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 847 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 864 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 932 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 1008 def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end
say_with_time(message) 链接
输出文本以及运行其块所花费的时间。如果块返回一个整数,则假定它是受影响的行数。
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1014 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() 链接
获取一个块作为参数,并抑制块生成的任何输出。
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 1024 def suppress_messages save, self.verbose = verbose, false yield ensure self.verbose = save end
up() 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 946 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[7.1]
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 923 def up_only(&block) execute_block(&block) unless reverting? end
write(text = "") 链接
来源:显示 | 在 GitHub 上
# File activerecord/lib/active_record/migration.rb, line 996 def write(text = "") puts(text) if verbose end