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_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(*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.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 中,您仍然可以打开自己的事务。
- MODULE ActiveRecord::Migration::Compatibility
- CLASS ActiveRecord::Migration::CheckPending
- CLASS ActiveRecord::Migration::CommandRecorder
- #
- A
- C
- D
- E
- L
- M
- N
- P
- R
- S
- U
- V
- W
Attributes
| [RW] | name | |
| [RW] | version |
类公共方法
[](version) Link
check_all_pending!() Link
如果环境中所有数据库配置都有待处理的迁移,则会引发 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() Link
disable_ddl_transaction!() Link
禁用围绕此迁移的事务。即使在调用 disable_ddl_transaction! 后,您仍然可以创建自己的事务。
有关更多详细信息,请阅读上面的“事务性迁移”部分。
load_schema_if_pending!() Link
migrate(direction) Link
new(name = self.class.name, version = nil) Link
verbose Link
指定迁移是否将它们正在执行的操作写入控制台,同时附带描述每个步骤所花费时间的基准测试。默认为 true。
实例公共方法
announce(message) Link
connection() Link
connection_pool() Link
copy(destination, sources, options = {}) Link
# 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() Link
exec_migration(conn, direction) Link
# 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() Link
method_missing(method, *arguments, &block) Link
# 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) Link
在指定方向执行此迁移
# 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) Link
确定下一个迁移的版本号。
proper_table_name(name, options = {}) Link
根据 Active Record 对象查找正确的表名。使用 Active Record 对象自己的 `table_name`,或使用传入选项中的前缀/后缀。
reversible() Link
用于指定一个可以双向执行的操作。调用所产生的对象的 `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
revert(*migration_classes, &block) Link
反转给定块和给定迁移的迁移命令。
以下迁移将在向上迁移时删除“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?() Link
run(*migration_classes) Link
运行给定的迁移类。最后一个参数可以指定选项:
-
: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) Link
接受一个消息参数并按原样输出。可以传递第二个布尔参数来指定是否缩进。
say_with_time(message) Link
输出文本以及运行其块所花费的时间。如果块返回一个整数,它将假定为受影响的行数。
# 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() Link
接受一个块作为参数,并抑制该块生成的任何输出。
up() Link
up_only(&block) Link
用于指定一个仅在向上迁移时运行的操作(例如,用初始值填充新列)。
在下面的示例中,对于所有现有记录,新列 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