更多内容请访问 rubyonrails.org:

Active Record 迁移

迁移是 Active Record 的一个功能,它允许您随着时间的推移演变数据库 schema。迁移不是用纯 SQL 编写 schema 修改,而是允许您使用 Ruby 领域特定语言 (DSL) 来描述对表的更改。

阅读本指南后,您将了解

  • 您可以使用哪些生成器来创建迁移。
  • Active Record 提供了哪些方法来操作您的数据库。
  • 如何更改现有迁移并更新您的 schema。
  • 迁移与 schema.rb 的关系。
  • 如何保持引用完整性。

1. 迁移概述

迁移是一种方便的、可重现的随着时间推移演变数据库 schema 的方式。它们使用 Ruby DSL,这样您就不必手动编写 SQL,从而使您的 schema 和更改独立于数据库。我们建议您阅读 Active Record 基础知识Active Record 关联 的指南,以了解更多此处提到的一些概念。

您可以将每次迁移视为数据库的一个新“版本”。一个 schema 最初是空的,每次迁移都会修改它以添加或删除表、列或索引。Active Record 知道如何沿着这个时间线更新您的 schema,将其从历史中的任何点带到最新版本。阅读更多关于Rails 如何知道在时间线中运行哪个迁移

Active Record 更新您的 db/schema.rb 文件以匹配数据库的最新结构。这是一个迁移的示例

# db/migrate/20240502100843_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

此迁移添加了一个名为 products 的表,其中包含一个名为 name 的字符串列和一个名为 description 的文本列。还将隐式添加一个名为 id 的主键列,因为它是所有 Active Record 模型的默认主键。timestamps 宏添加了两个列,created_atupdated_at。这些特殊列由 Active Record 在存在时自动管理。

# db/schema.rb
ActiveRecord::Schema[8.1].define(version: 2024_05_02_100843) do
  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "products", force: :cascade do |t|
    t.string "name"
    t.text "description"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end

我们定义了我们希望在未来发生的变化。在此迁移运行之前,将没有表。运行之后,该表将存在。Active Record 也知道如何回滚此迁移;如果我们回滚此迁移,它将删除该表。在回滚部分中阅读更多关于回滚迁移的信息。

定义了我们希望在未来发生的变化之后,考虑迁移的可逆性至关重要。虽然 Active Record 可以管理迁移的正向进程,确保表的创建,但可逆性概念变得至关重要。通过可逆迁移,迁移不仅在应用时创建表,还支持平滑的回滚功能。如果回滚上述迁移,Active Record 会智能地处理表的删除,在整个过程中保持数据库一致性。有关更多详细信息,请参阅反转迁移部分

2. 生成迁移文件

2.1. 创建独立迁移

迁移作为文件存储在 db/migrate 目录中,每个迁移类一个文件。

文件的形式为 YYYYMMDDHHMMSS_create_products.rb,它包含一个标识迁移的 UTC 时间戳,后跟一个下划线,再后跟迁移的名称。迁移类的名称(驼峰式版本)应与文件名的后半部分匹配。

例如,20240502100843_create_products.rb 应该定义类 CreateProducts,而 20240502101659_add_details_to_products.rb 应该定义类 AddDetailsToProducts。Rails 使用此时间戳来确定应运行哪个迁移以及以何种顺序运行,因此如果您从另一个应用程序复制迁移或自己生成文件,请注意它在顺序中的位置。您可以在Rails 迁移版本控制部分中阅读更多关于时间戳如何使用的信息。

您可以通过在 config/database.yml 中设置 migrations_paths 选项来覆盖存储迁移的目录。

生成迁移时,Active Record 自动将当前时间戳添加到迁移的文件名中。例如,运行以下命令将创建一个空的迁移文件,其中文件名由时间戳前缀加上迁移的下划线名称组成。

$ bin/rails generate migration AddPartNumberToProducts
# db/migrate/20240502101659_add_part_number_to_products.rb
class AddPartNumberToProducts < ActiveRecord::Migration[8.1]
  def change
  end
end

生成器不仅可以为文件名添加时间戳。根据命名约定和额外的(可选)参数,它还可以开始充实迁移。

以下部分将介绍基于约定和额外参数创建迁移的各种方法。

2.2. 创建新表

当您想在数据库中创建新表时,可以使用格式为“CreateXXX”的迁移,后跟列名和类型列表。这将生成一个迁移文件,该文件使用指定的列设置表。

$ bin/rails generate migration CreateProducts name:string part_number:string

生成

class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    create_table :products do |t|
      t.string :name
      t.string :part_number

      t.timestamps
    end
  end
end

如果您没有为字段指定类型(例如,name 而不是 name:string),Rails 将默认为 string 类型。

生成的文件及其内容只是一个起点,您可以根据需要通过编辑 db/migrate/YYYYMMDDHHMMSS_create_products.rb 文件来添加或删除它。

2.3. 添加列

当您想向数据库中的现有表添加新列时,可以使用格式为“AddColumnToTable”的迁移,后跟列名和类型列表。这将生成一个包含相应 add_column 语句的迁移文件。

$ bin/rails generate migration AddPartNumberToProducts part_number:string

这将生成以下迁移

class AddPartNumberToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :part_number, :string
  end
end

如果您想在新列上添加索引,您也可以这样做。

$ bin/rails generate migration AddPartNumberToProducts part_number:string:index

这将生成相应的 add_columnadd_index 语句

class AddPartNumberToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :part_number, :string
    add_index :products, :part_number
  end
end

受限于一个魔术生成的列。例如

$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal

这将生成一个 schema 迁移,它向 products 表添加两个附加列。

class AddDetailsToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :part_number, :string
    add_column :products, :price, :decimal
  end
end

2.4. 删除列

类似地,如果迁移名称的形式为“RemoveColumnFromTable”,并且后跟列名和类型列表,则将创建一个包含相应 remove_column 语句的迁移。

$ bin/rails generate migration RemovePartNumberFromProducts part_number:string

这将生成相应的 remove_column 语句

class RemovePartNumberFromProducts < ActiveRecord::Migration[8.1]
  def change
    remove_column :products, :part_number, :string
  end
end

2.5. 创建关联

Active Record 关联用于定义应用程序中不同模型之间的关系,允许它们通过关系相互交互,从而更容易处理相关数据。要了解更多关联信息,您可以参考 关联基础知识指南

关联的一个常见用例是创建表之间的外键引用。生成器接受 references 等列类型以方便此过程。引用是创建列、索引、外键甚至多态关联列的简写。

例如,

$ bin/rails generate migration AddUserRefToProducts user:references

生成以下 add_reference 调用

class AddUserRefToProducts < ActiveRecord::Migration[8.1]
  def change
    add_reference :products, :user, null: false, foreign_key: true
  end
end

上述迁移在 products 表中创建了一个名为 user_id 的外键,其中 user_id 是对 users 表中 id 列的引用。它还为 user_id 列创建了一个索引。schema 如下所示

  create_table "products", force: :cascade do |t|
    t.bigint "user_id", null: false
    t.index ["user_id"], name: "index_products_on_user_id"
  end

belongs_toreferences 的别名,因此上述可以替代地写为

$ bin/rails generate migration AddUserRefToProducts user:belongs_to

生成与上述相同的迁移和 schema。

如果名称中包含 JoinTable,则还有一个生成器将生成连接表

$ bin/rails generate migration CreateJoinTableUserProduct user product

将生成以下迁移

class CreateJoinTableUserProduct < ActiveRecord::Migration[8.1]
  def change
    create_join_table :users, :products do |t|
      # t.index [:user_id, :product_id]
      # t.index [:product_id, :user_id]
    end
  end
end

2.6. 其他创建迁移的生成器

除了 migration 生成器,modelresourcescaffold 生成器将创建适合添加新模型的迁移。此迁移将已包含创建相关表的指令。如果您告诉 Rails 您想要的列,那么还将创建用于添加这些列的语句。例如,运行

$ bin/rails generate model Product name:string description:text

这将创建一个如下所示的迁移

class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

您可以根据需要附加任意数量的列名/类型对。

2.7. 传递修饰符

生成迁移时,您可以直接在命令行上传递常用的类型修饰符。这些修饰符用大括号括起来,并跟在字段类型之后,允许您定制数据库列的特性,而无需事后手动编辑迁移文件。

例如,运行

$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

将生成一个如下所示的迁移

class AddDetailsToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :price, :decimal, precision: 5, scale: 2
    add_reference :products, :supplier, polymorphic: true
  end
end

可以使用 ! 快捷方式从命令行施加 NOT NULL 约束

$ bin/rails generate migration AddEmailToUsers email:string!

将生成此迁移

class AddEmailToUsers < ActiveRecord::Migration[8.1]
  def change
    add_column :users, :email, :string, null: false
  end
end

有关生成器的进一步帮助,请运行 bin/rails generate --help。或者,您也可以运行 bin/rails generate model --helpbin/rails generate migration --help 以获取特定生成器的帮助。

3. 更新迁移

使用上述部分中的某个生成器创建迁移文件后,您可以在 db/migrate 文件夹中更新生成的迁移文件,以定义您希望对数据库 schema 进行的进一步更改。

3.1. 创建表

create_table 方法是最基本的迁移类型之一,但在大多数情况下,将通过使用模型、资源或 scaffold 生成器为您生成。典型用法是

create_table :products do |t|
  t.string :name
end

此方法创建一个名为 products 的表,其中包含一个名为 name 的列。

3.1.1. 关联

如果您正在为具有关联的模型创建表,您可以使用 :references 类型来创建适当的列类型。例如

create_table :products do |t|
  t.references :category
end

这将创建一个 category_id 列。或者,您可以使用 belongs_to 作为 references 的别名

create_table :products do |t|
  t.belongs_to :category
end

您还可以使用 :polymorphic 选项指定列类型和索引创建

create_table :taggings do |t|
  t.references :taggable, polymorphic: true
end

这将创建 taggable_idtaggable_type 列和相应的索引。

3.1.2. 主键

默认情况下,create_table 将隐式为您创建一个名为 id 的主键。您可以使用 :primary_key 选项更改列的名称,如下所示

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users, primary_key: "user_id" do |t|
      t.string :username
      t.string :email
      t.timestamps
    end
  end
end

这将产生以下 schema

create_table "users", primary_key: "user_id", force: :cascade do |t|
  t.string "username"
  t.string "email"
  t.datetime "created_at", precision: 6, null: false
  t.datetime "updated_at", precision: 6, null: false
end

您还可以将数组传递给 :primary_key 以用于复合主键。阅读更多关于 复合主键 的信息。

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users, primary_key: [:id, :name] do |t|
      t.string :name
      t.string :email
      t.timestamps
    end
  end
end

如果您根本不需要主键,您可以传递选项 id: false

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users, id: false do |t|
      t.string :username
      t.string :email
      t.timestamps
    end
  end
end

3.1.3. 数据库选项

如果您需要传递数据库特定选项,您可以在 :options 选项中放置一个 SQL 片段。例如

create_table :products, options: "ENGINE=BLACKHOLE" do |t|
  t.string :name, null: false
end

这将在用于创建表的 SQL 语句中附加 ENGINE=BLACKHOLE

可以通过将 index: true 或选项哈希传递给 :index 选项来在 create_table 块中创建的列上创建索引

create_table :users do |t|
  t.string :name, index: true
  t.string :email, index: { unique: true, name: "unique_emails" }
end

3.1.4. 注释

您可以传递 :comment 选项以及表的任何描述,这些描述将存储在数据库本身中,并且可以使用数据库管理工具(例如 MySQL Workbench 或 PgAdmin III)查看。注释可以帮助团队成员更好地理解数据模型并在具有大型数据库的应用程序中生成文档。目前只有 MySQL 和 PostgreSQL 适配器支持注释。

class AddDetailsToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :price, :decimal, precision: 8, scale: 2, comment: "The price of the product in USD"
    add_column :products, :stock_quantity, :integer, comment: "The current stock quantity of the product"
  end
end

3.2. 创建连接表

迁移方法 create_join_table 创建一个 HABTM (has and belongs to many) 连接表。典型用法是

create_join_table :products, :categories

此迁移将创建一个 categories_products 表,其中包含两个列 category_idproduct_id

这些列默认将选项 :null 设置为 false,这意味着您必须提供一个值才能将记录保存到此表中。这可以通过指定 :column_options 选项来覆盖

create_join_table :products, :categories, column_options: { null: true }

默认情况下,连接表的名称来自提供给 create_join_table 的前两个参数的并集,按词法顺序。在这种情况下,表将被命名为 categories_products

模型名称之间的优先级使用 String<=> 运算符计算。这意味着如果字符串长度不同,并且在比较最短长度时字符串相等,则较长的字符串被认为具有比短字符串更高的词法优先级。例如,人们会期望表“paper_boxes”和“papers”生成连接表名称“papers_paper_boxes”,因为名称“paper_boxes”的长度,但实际上它生成连接表名称“paper_boxes_papers”(因为下划线“_”在常见的编码中词法上小于“s”)。

要自定义表的名称,请提供 :table_name 选项

create_join_table :products, :categories, table_name: :categorization

这将创建一个名为 categorization 的连接表。

此外,create_join_table 接受一个块,您可以使用它来添加索引(默认不创建)或任何您选择的附加列。

create_join_table :products, :categories do |t|
  t.index :product_id
  t.index :category_id
end

3.3. 更改表

如果您想就地更改现有表,可以使用 change_table

它的用法与 create_table 类似,但在块中生成的对象可以访问许多特殊函数,例如

change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end

此迁移将删除 descriptionname 列,创建一个名为 part_number 的新字符串列并添加索引。最后,它将 upccode 列重命名为 upc_code

3.4. 更改列

类似于我们之前介绍的 remove_columnadd_column 方法,Rails 还提供了 change_column 迁移方法。

change_column :products, :part_number, :text

这将把 products 表上的 part_number 列更改为 :text 字段。

change_column 命令是不可逆的。为了确保您的迁移可以安全回滚,您需要提供自己的 reversible 迁移。有关更多详细信息,请参阅可逆迁移部分

除了 change_columnchange_column_nullchange_column_default 方法用于更改列的 null 约束和默认值。

change_column_default :products, :approved, from: true, to: false

这将把 :approved 字段的默认值从 true 更改为 false。此更改仅适用于未来的记录,任何现有记录都不会更改。使用 change_column_null 更改 null 约束。

change_column_null :products, :name, false

这会将 products 上的 :name 字段设置为 NOT NULL 列。此更改也适用于现有记录,因此您需要确保所有现有记录都具有 NOT NULL:name

将 null 约束设置为 true 意味着该列将接受 null 值,否则将应用 NOT NULL 约束,并且必须传递一个值才能将记录持久化到数据库。

您也可以将上述 change_column_default 迁移写为 change_column_default :products, :approved, false,但这与前面的示例不同,这将使您的迁移不可逆。

3.5. 列修饰符

创建或更改列时可以应用列修饰符

  • comment 为列添加注释。
  • collationstringtext 列指定排序规则。
  • default 允许为列设置默认值。请注意,如果您使用的是动态值(例如日期),默认值将只计算一次(即在应用迁移的日期)。对于 NULL 使用 nil
  • limit 设置 string 列的最大字符数和 text/binary/integer 列的最大字节数。
  • null 允许或不允许列中的 NULL 值。
  • precision 指定 decimal/numeric/datetime/time 列的精度。
  • scale 指定 decimalnumeric 列的比例,表示小数点后的位数。

对于 add_columnchange_column,没有添加索引的选项。它们需要使用 add_index 单独添加。

某些适配器可能支持其他选项;有关更多信息,请参阅适配器特定的 API 文档。

生成迁移时不能通过命令行指定 default

3.6. 引用

add_reference 方法允许创建适当命名的列,作为多个关联之间的连接。

add_reference :users, :role

此迁移将在 users 表中创建一个名为 role_id 的外键列。role_id 是对 roles 表中 id 列的引用。此外,它还为 role_id 列创建了一个索引,除非使用 index: false 选项明确指示不这样做。

另请参阅 Active Record 关联 指南以了解更多信息。

add_belongs_to 方法是 add_reference 的别名。

add_belongs_to :taggings, :taggable, polymorphic: true

多态选项将在 taggings 表上创建两个可用于多态关联的列:taggable_typetaggable_id

参阅此指南以了解更多关于 多态关联 的信息。

可以使用 foreign_key 选项创建外键。

add_reference :users, :role, foreign_key: true

有关更多 add_reference 选项,请访问 API 文档

引用也可以删除

remove_reference :products, :user, foreign_key: true, index: false

3.7. 外键

虽然不是必需的,但您可能希望添加外键约束以保证引用完整性

add_foreign_key :articles, :authors

add_foreign_key 调用向 articles 表添加了一个新约束。该约束保证 authors 表中存在一个行,其中 id 列与 articles.author_id 匹配,以确保 articles 表中列出的所有审阅者都是 authors 表中列出的有效作者。

在迁移中使用 references 时,您将在表中创建新列,并且可以选择使用 foreign_key: true 向该列添加外键。但是,如果您想向现有列添加外键,可以使用 add_foreign_key

如果无法从具有引用主键的表中派生出我们要添加外键的表的列名,则可以使用 :column 选项指定列名。此外,如果引用的主键不是 :id,则可以使用 :primary_key 选项。

例如,要在 articles.reviewer 上添加引用 authors.email 的外键

add_foreign_key :articles, :authors, column: :reviewer, primary_key: :email

这将向 articles 表添加一个约束,该约束保证 authors 表中存在一个行,其中 email 列与 articles.reviewer 字段匹配。

add_foreign_key 支持其他几个选项,例如 nameon_deleteif_not_existsvalidatedeferrable

外键也可以使用 remove_foreign_key 删除

# let Active Record figure out the column name
remove_foreign_key :accounts, :branches

# remove foreign key for a specific column
remove_foreign_key :accounts, column: :owner_id

Active Record 只支持单列外键。复合外键需要使用 executestructure.sql。参阅 Schema Dumping and You

3.8. 复合主键

有时,单个列的值不足以唯一标识表的每一行,但两个或更多列的组合确实可以唯一标识它。在使用没有单个 id 列作为主键的旧数据库 schema 时,或者在分片或多租户的 schema 更改时,可能会出现这种情况。

您可以通过将 :primary_key 选项传递给 create_table 并带有一个数组值来创建具有复合主键的表

class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    create_table :products, primary_key: [:customer_id, :product_sku] do |t|
      t.integer :customer_id
      t.string :product_sku
      t.text :description
    end
  end
end

具有复合主键的表需要将数组值而不是整数 ID 传递给许多方法。另请参阅 Active Record 复合主键 指南以了解更多信息。

3.9. 执行 SQL

如果 Active Record 提供的辅助方法不够,您可以使用 execute 方法执行 SQL 命令。例如,

class UpdateProductPrices < ActiveRecord::Migration[8.1]
  def up
    execute "UPDATE products SET price = 'free'"
  end

  def down
    execute "UPDATE products SET price = 'original_price' WHERE price = 'free';"
  end
end

在此示例中,我们正在将 products 表的 price 列更新为所有记录的“free”。

应谨慎对待直接在迁移中修改数据。考虑这是否是您用例的最佳方法,并注意潜在的缺点,例如复杂性和维护开销增加、数据完整性和数据库可移植性风险。有关更多详细信息,请参阅数据迁移文档

有关单个方法的更多详细信息和示例,请查看 API 文档。

特别是 ActiveRecord::ConnectionAdapters::SchemaStatements 的文档,它提供了 changeupdown 方法中可用的方法。

有关 create_table 生成的对象的可用方法,请参阅 ActiveRecord::ConnectionAdapters::TableDefinition

以及 change_table 生成的对象的可用方法,请参阅 ActiveRecord::ConnectionAdapters::Table

3.10. 使用 change 方法

change 方法是编写迁移的主要方式。它适用于 Active Record 知道如何自动回滚迁移操作的大多数情况。以下是 change 支持的一些操作

change_table 也是可逆的,只要该块只调用上面列出的可逆操作。

如果您需要使用任何其他方法,您应该使用 reversible 或编写 updown 方法,而不是使用 change 方法。

3.11. 使用 reversible

如果您希望迁移执行 Active Record 不知道如何回滚的操作,那么您可以使用 reversible 来指定在运行迁移时执行什么操作,以及在回滚时执行什么操作。

class ChangeProductsPrice < ActiveRecord::Migration[8.1]
  def change
    reversible do |direction|
      change_table :products do |t|
        direction.up   { t.change :price, :string }
        direction.down { t.change :price, :integer }
      end
    end
  end
end

此迁移会将 price 列的类型更改为字符串,或者在迁移回滚时恢复为整数。请注意分别传递给 direction.updirection.down 的块。

或者,您可以使用 updown 而不是 change

class ChangeProductsPrice < ActiveRecord::Migration[8.1]
  def up
    change_table :products do |t|
      t.change :price, :string
    end
  end

  def down
    change_table :products do |t|
      t.change :price, :integer
    end
  end
end

此外,reversible 在执行原始 SQL 查询或执行在 ActiveRecord 方法中没有直接等效的数据库操作时很有用。您可以使用 reversible 来指定在运行迁移时执行什么操作,以及在回滚时执行什么操作。例如

class ExampleMigration < ActiveRecord::Migration[8.1]
  def change
    create_table :distributors do |t|
      t.string :zipcode
    end

    reversible do |direction|
      direction.up do
        # create a distributors view
        execute <<-SQL
          CREATE VIEW distributors_view AS
          SELECT id, zipcode
          FROM distributors;
        SQL
      end
      direction.down do
        execute <<-SQL
          DROP VIEW distributors_view;
        SQL
      end
    end

    add_column :users, :address, :string
  end
end

使用 reversible 还会确保指令以正确的顺序执行。如果回滚了上面的示例迁移,则在删除 users.address 列之后并在删除 distributors 表之前运行 down 块。

3.12. 使用 up/down 方法

您也可以使用旧式迁移,使用 updown 方法,而不是 change 方法。

up 方法应该描述您希望对 schema 进行的转换,而迁移的 down 方法应该回滚 up 方法所做的转换。换句话说,如果您先执行 up,然后执行 down,数据库 schema 应该保持不变。

例如,如果您在 up 方法中创建了一个表,您应该在 down 方法中将其删除。明智的做法是以与 up 方法中进行转换的精确相反的顺序执行转换。reversible 部分中的示例等效于

class ExampleMigration < ActiveRecord::Migration[8.1]
  def up
    create_table :distributors do |t|
      t.string :zipcode
    end

    # create a distributors view
    execute <<-SQL
      CREATE VIEW distributors_view AS
      SELECT id, zipcode
      FROM distributors;
    SQL

    add_column :users, :address, :string
  end

  def down
    remove_column :users, :address

    execute <<-SQL
      DROP VIEW distributors_view;
    SQL

    drop_table :distributors
  end
end

3.13. 抛出错误以防止回滚

有时您的迁移会做一些无法逆转的事情;例如,它可能会销毁一些数据。

在这种情况下,您可以在 down 块中引发 ActiveRecord::IrreversibleMigration

class IrreversibleMigrationExample < ActiveRecord::Migration[8.1]
  def up
    drop_table :example_table
  end

  def down
    raise ActiveRecord::IrreversibleMigration, "This migration cannot be reverted because it destroys data."
  end
end

如果有人尝试回滚您的迁移,将显示一条错误消息,指出无法完成。

3.14. 回滚以前的迁移

您可以使用 Active Record 的回滚迁移能力,方法是使用 revert 方法

require_relative "20121212123456_example_migration"

class FixupExampleMigration < ActiveRecord::Migration[8.1]
  def change
    revert ExampleMigration

    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

revert 方法还接受一个指令块来反转。这对于回滚以前迁移的选定部分可能很有用。

例如,假设 ExampleMigration 已提交,后来决定不再需要 Distributors 视图。

class DontUseDistributorsViewMigration < ActiveRecord::Migration[8.1]
  def change
    revert do
      # copy-pasted code from ExampleMigration
      create_table :distributors do |t|
        t.string :zipcode
      end

      reversible do |direction|
        direction.up do
          # create a distributors view
          execute <<-SQL
            CREATE VIEW distributors_view AS
            SELECT id, zipcode
            FROM distributors;
          SQL
        end
        direction.down do
          execute <<-SQL
            DROP VIEW distributors_view;
          SQL
        end
      end

      # The rest of the migration was ok
    end
  end
end

相同的迁移也可以不使用 revert 来编写,但这将涉及更多步骤

  1. 反转 create_tablereversible 的顺序。
  2. create_table 替换为 drop_table
  3. 最后,将 up 替换为 down,反之亦然。

所有这些都由 revert 处理。

4. 运行迁移

Rails 提供了一组命令来运行某些迁移集。

您将使用的第一个与迁移相关的 rails 命令可能是 bin/rails db:migrate。它最基本的用法是运行所有尚未运行的迁移的 changeup 方法。如果没有此类迁移,它将退出。它将根据迁移的日期按顺序运行这些迁移。

请注意,运行 db:migrate 命令还会调用 db:schema:dump 命令,该命令将更新您的 db/schema.rb 文件以匹配数据库的结构。

如果您指定目标版本,Active Record 将运行所需的迁移(更改、向上、向下),直到达到指定版本。版本是迁移文件名的数字前缀。例如,要迁移到版本 20240428000000,请运行

$ bin/rails db:migrate VERSION=20240428000000

如果版本 20240428000000 大于当前版本(即,它正在向上迁移),这将运行所有迁移(包括 20240428000000)的 change(或 up)方法,并且不会执行任何后续迁移。如果向下迁移,这将运行所有迁移(不包括 20240428000000)的 down 方法。

4.1. 回滚

一项常见任务是回滚上次迁移。例如,如果您在其中犯了错误并希望纠正它。您无需追踪与上次迁移相关的版本号,您可以运行

$ bin/rails db:rollback

这将回滚最新迁移,无论是通过回滚 change 方法还是运行 down 方法。如果您需要撤消多个迁移,可以提供 STEP 参数

$ bin/rails db:rollback STEP=3

最后 3 次迁移将被回滚。

在某些情况下,您修改了本地迁移并希望在再次向上迁移之前回滚该特定迁移,您可以使用 db:migrate:redo 命令。与 db:rollback 命令一样,如果您需要回退一个以上版本,可以使用 STEP 参数,例如

$ bin/rails db:migrate:redo STEP=3

您可以使用 db:migrate 获得相同的结果。但是,这些是为了方便起见,这样您就不需要明确指定要迁移到的版本。

4.1.1. 事务

在支持 DDL 事务的数据库中,在单个事务中更改 schema,每个迁移都包裹在一个事务中。

事务确保如果迁移中途失败,任何成功应用的更改都将回滚,从而保持数据库一致性。这意味着事务中的所有操作要么都成功执行,要么都不执行,从而防止在事务期间发生错误时数据库处于不一致状态。

如果数据库不支持使用更改 schema 的语句进行 DDL 事务,那么当迁移失败时,成功的部分将不会回滚。您将不得不手动回滚更改。

但是,有些查询不能在事务中执行,对于这些情况,您可以使用 disable_ddl_transaction! 关闭自动事务

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! 的迁移中,您仍然可以打开自己的事务。

4.2. 设置数据库

bin/rails db:setup 命令将创建数据库、加载 schema 并使用种子数据对其进行初始化。

4.3. 准备数据库

bin/rails db:prepare 命令类似于 bin/rails db:setup,但它是幂等的,因此可以安全地多次调用,但它只会执行必要的任务一次。

  • 如果数据库尚未创建,该命令将像 bin/rails db:setup 一样运行。
  • 如果数据库存在但尚未创建表,该命令将加载 schema、运行任何待处理的迁移、转储更新后的 schema,最后加载种子数据。有关更多详细信息,请参阅播种数据文档
  • 如果数据库和表存在,该命令将不执行任何操作。

一旦数据库和表存在,db:prepare 任务将不会尝试重新加载种子数据,即使以前加载的种子数据或现有种子文件已被更改或删除。要重新加载种子数据,您可以手动运行 bin/rails db:seed:replant

此任务仅在创建的数据库或表之一是环境的主数据库或配置为 seeds: true 时加载种子。

4.4. 重置数据库

bin/rails db:reset 命令将删除数据库并再次设置它。这在功能上等同于 bin/rails db:drop db:setup

这与运行所有迁移不同。它只会使用当前 db/schema.rbdb/structure.sql 文件的内容。如果迁移无法回滚,bin/rails db:reset 可能对您没有帮助。要了解有关转储 schema 的更多信息,请参阅Schema Dumping and You 部分。

如果您需要 db:reset 的替代方案,它明确运行所有迁移,请考虑使用 bin/rails db:migrate:reset 命令。如果需要,您可以在此命令之后运行 bin/rails db:seed

bin/rails db:reset 使用当前 schema 重建数据库。另一方面,bin/rails db:migrate:reset 从头开始重播所有迁移,这可能导致 schema 漂移,例如,如果迁移已被更改、重新排序或删除。

4.5. 运行特定迁移

如果您需要向上或向下运行特定迁移,db:migrate:updb:migrate:down 命令将执行此操作。只需指定适当的版本,相应的迁移将调用其 changeupdown 方法,例如

$ bin/rails db:migrate:up VERSION=20240428000000

通过运行此命令,将为版本为“20240428000000”的迁移执行 change 方法(或 up 方法)。

首先,此命令将检查迁移是否存在以及是否已执行,如果是,它将不执行任何操作。

如果指定版本不存在,Rails 将抛出异常。

$ bin/rails db:migrate VERSION=00000000000000
rails aborted!
ActiveRecord::UnknownMigrationVersionError:

No migration with version number 00000000000000.

4.6. 在不同环境中运行迁移

默认情况下,运行 bin/rails db:migrate 将在 development 环境中运行。

要针对另一个环境运行迁移,您可以在运行命令时使用 RAILS_ENV 环境变量指定它。例如,要针对 test 环境运行迁移,您可以运行

$ bin/rails db:migrate RAILS_ENV=test

4.7. 更改运行迁移的输出

默认情况下,迁移会准确地告诉您它们正在做什么以及花了多长时间。创建表和添加索引的迁移可能会产生如下输出

==  CreateProducts: migrating =================================================
-- create_table(:products)
   -> 0.0028s
==  CreateProducts: migrated (0.0028s) ========================================

迁移中提供了几种方法,允许您控制所有这些

方法 用途
suppress_messages 接受一个块作为参数并抑制该块生成的任何输出。
say 接受消息参数并按原样输出。可以传递第二个布尔参数以指定是否缩进。
say_with_time 输出文本以及运行其块所需的时间。如果块返回一个整数,它假定它是受影响的行数。

例如,以下迁移

class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    suppress_messages do
      create_table :products do |t|
        t.string :name
        t.text :description
        t.timestamps
      end
    end

    say "Created a table"

    suppress_messages { add_index :products, :name }
    say "and an index!", true

    say_with_time "Waiting for a while" do
      sleep 10
      250
    end
  end
end

这将生成以下输出

==  CreateProducts: migrating =================================================
-- Created a table
   -> and an index!
-- Waiting for a while
   -> 10.0013s
   -> 250 rows
==  CreateProducts: migrated (10.0054s) =======================================

如果您不希望 Active Record 输出任何内容,那么运行 bin/rails db:migrate VERBOSE=false 将抑制所有输出。

4.8. Rails 迁移版本控制

Rails 通过数据库中的 schema_migrations 表跟踪已运行的迁移。当您运行迁移时,Rails 会在 schema_migrations 表中插入一行,其中包含迁移的版本号,存储在 version 列中。这允许 Rails 确定哪些迁移已应用于数据库。

例如,如果您有一个名为 20240428000000_create_users.rb 的迁移文件,Rails 将从文件名中提取版本号 (20240428000000),并在迁移成功执行后将其插入到 schema_migrations 表中。

您可以直接在数据库管理工具中或通过使用 Rails console 查看 schema_migrations 表的内容

rails dbconsole

然后,在数据库控制台中,您可以查询 schema_migrations 表

SELECT * FROM schema_migrations;

这将显示已应用于数据库的所有迁移版本号列表。Rails 使用此信息来确定当您运行 rails db:migrate 或 rails db:migrate:up 命令时需要运行哪些迁移。

5. 更改现有迁移

有时您在编写迁移时会犯错误。如果您已经运行了迁移,那么您不能简单地编辑迁移并再次运行迁移:Rails 认为它已经运行了迁移,因此当您运行 bin/rails db:migrate 时将不执行任何操作。您必须回滚迁移(例如使用 bin/rails db:rollback),编辑您的迁移,然后运行 bin/rails db:migrate 以运行更正的版本。

通常,编辑已提交到源代码管理中的现有迁移不是一个好主意。您将为自己和同事制造额外的工作,并且如果现有版本的迁移已在生产机器上运行,则会造成严重的麻烦。相反,您应该编写一个执行所需更改的新迁移。

但是,编辑尚未提交到源代码管理(或者,更一般地说,尚未传播到您的开发机器之外)的刚生成的迁移是很常见的。

revert 方法在编写新迁移以完全或部分撤消以前的迁移时很有帮助(请参阅上面的回滚以前的迁移)。

6. Schema 导出与您

6.1. Schema 文件有什么用?

迁移,无论它们多么强大,都不是您的数据库 schema 的权威来源。您的数据库仍然是真相的来源。

默认情况下,Rails 生成 db/schema.rb,它尝试捕获数据库 schema 的当前状态。

通过 bin/rails db:schema:load 加载 schema 文件来创建应用程序数据库的新实例,比重放整个迁移历史记录更快,错误更少。旧迁移如果这些迁移使用不断变化的外部依赖项或依赖于与迁移独立演变的应用程序代码,则可能无法正确应用。

Schema 文件在您想快速查看 Active Record 对象具有哪些属性时也很有用。此信息不在模型的代码中,并且经常分散在多个迁移中,但信息在 schema 文件中很好地总结了。

6.2. Schema 导出类型

Rails 生成的 schema 导出格式由 config/application.rb 中定义的 config.active_record.schema_format 设置或数据库配置中的 schema_format 值控制。默认情况下,格式为 :ruby,或者可以设置为 :sql

6.2.1. 使用默认的 :ruby schema

当选择 :ruby 时,schema 存储在 db/schema.rb 中。如果您查看此文件,您会发现它看起来非常像一个非常大的迁移

ActiveRecord::Schema[8.1].define(version: 2008_09_06_171750) do
  create_table "authors", force: true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "products", force: true do |t|
    t.string   "name"
    t.text     "description"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.string   "part_number"
  end
end

在许多方面,这正是它的本质。此文件是通过检查数据库并使用 create_tableadd_index 等表达其结构来创建的。

6.2.2. 使用 :sql schema 导出器

但是,db/schema.rb 无法表达您的数据库可能支持的所有内容,例如触发器、序列、存储过程等。

虽然迁移可以使用 execute 来创建 Ruby 迁移 DSL 不支持的数据库构造,但这些构造可能无法由 schema 导出器重构。

如果您正在使用这些功能,您应该将 schema 格式设置为 :sql,以便获得一个准确的 schema 文件,该文件可用于创建新的数据库实例。

当 schema 格式设置为 :sql 时,数据库结构将使用数据库特定工具转储到 db/structure.sql 中。例如,对于 PostgreSQL,使用 pg_dump 工具。对于 MySQL 和 MariaDB,此文件将包含各种表的 SHOW CREATE TABLE 输出。

要从 db/structure.sql 加载 schema,请运行 bin/rails db:schema:load。加载此文件是通过执行其中包含的 SQL 语句来完成的。根据定义,这将创建数据库结构的完美副本。

6.3. Schema 导出和源代码控制

因为 schema 文件通常用于创建新数据库,所以强烈建议您将 schema 文件签入源代码管理。

当两个分支修改 schema 时,您的 schema 文件中可能会发生合并冲突。要解决这些冲突,请运行 bin/rails db:migrate 以重新生成 schema 文件。

新生成的 Rails 应用程序的 git 树中已经包含 migrations 文件夹,所以您所要做的就是确保添加您添加的任何新迁移并提交它们。

7. Active Record 和引用完整性

Active Record 模式建议智能主要驻留在模型中,而不是数据库中。因此,像触发器或约束这样的功能,将一些智能委托回数据库,并不总是受青睐。

validates :foreign_key, uniqueness: true 这样的验证是模型可以强制执行数据完整性的一种方式。关联上的 :dependent 选项允许模型在父对象被销毁时自动销毁子对象。像任何在应用程序级别操作的东西一样,这些不能保证引用完整性,因此有些人会用数据库中的外键约束来补充它们。

实际上,外键约束和唯一索引通常在数据库级别强制执行时被认为更安全。尽管 Active Record 不直接支持使用这些数据库级别功能,但您仍然可以使用 execute 方法运行任意 SQL 命令。

值得强调的是,虽然 Active Record 模式强调将智能保留在模型中,但忽略在数据库级别实现外键和唯一约束可能会导致完整性问题。因此,建议在适当的情况下,用数据库级别约束补充 AR 模式。这些约束应该在您的代码中明确定义其对应项,使用关联和验证来确保跨应用程序和数据库层的数据完整性。

8. 迁移和种子数据

Rails 迁移功能的主要目的是通过一致的过程发布修改 schema 的命令。迁移也可以用于添加或修改数据。这对于不能被销毁和重新创建的现有数据库很有用,例如生产数据库。

class AddInitialProducts < ActiveRecord::Migration[8.1]
  def up
    5.times do |i|
      Product.create(name: "Product ##{i}", description: "A product.")
    end
  end

  def down
    Product.delete_all
  end
end

为了在数据库创建后添加初始数据,Rails 有一个内置的“种子”功能,可以加快此过程。这在开发和测试环境中频繁重新加载数据库时,或在为生产设置初始数据时特别有用。

要开始使用此功能,请打开 db/seeds.rb 并添加一些 Ruby 代码,然后运行 bin/rails db:seed

这里的代码应该是幂等的,以便它可以在任何环境中随时执行。

["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
  MovieGenre.find_or_create_by!(name: genre_name)
end

这通常是设置空白应用程序数据库的一种更简洁的方式。

9. 旧迁移

db/schema.rbdb/structure.sql 是数据库当前状态的快照,是重建该数据库的权威来源。这使得可以删除或修剪旧的迁移文件。

当您删除 db/migrate/ 目录中的迁移文件时,在这些文件仍然存在时运行 bin/rails db:migrate 的任何环境都将保留对内部 Rails 数据库表 schema_migrations 中特定于它们的迁移时间戳的引用。您可以在Rails 迁移版本控制部分中阅读更多相关信息。

如果您运行 bin/rails db:migrate:status 命令(显示每个迁移的状态(up 或 down)),您应该在任何已删除的迁移文件旁边看到 ********** NO FILE **********,这些文件曾经在特定环境中执行过,但在 db/migrate/ 目录中找不到。

9.1. 来自引擎的迁移

处理来自 引擎 的迁移时,有一个需要注意的地方。用于从引擎安装迁移的 Rake 任务是幂等的,这意味着无论调用多少次,它们都会产生相同的结果。由于之前安装而存在于父应用程序中的迁移会被跳过,而缺失的迁移会复制并带有新的前导时间戳。如果您删除了旧的引擎迁移并再次运行安装任务,您将获得带有新时间戳的新文件,并且 db:migrate 将尝试再次运行它们。

因此,您通常希望保留来自引擎的迁移。它们有这样一个特殊的注释

# This migration comes from blorgh (originally 20210621082949)

10. 杂项

10.1. 使用 UUID 代替 ID 作为主键

默认情况下,Rails 使用自增整数作为数据库记录的主键。但是,在某些情况下,使用通用唯一标识符 (UUID) 作为主键可能是有利的,特别是在分布式系统或需要与外部服务集成时。UUID 提供全局唯一标识符,而无需依赖集中式机构来生成 ID。

10.1.1. 在 Rails 中启用 UUID

在 Rails 应用程序中使用 UUID 之前,您需要确保数据库支持存储它们。此外,您可能需要配置数据库适配器以与 UUID 配合使用。

如果您使用的是 PostgreSQL 13 之前的版本,您可能仍然需要启用 pgcrypto 扩展才能访问 gen_random_uuid() 函数。

  1. Rails 配置

    在您的 Rails 应用程序配置文件(config/application.rb)中,添加以下行以配置 Rails 默认生成 UUID 作为主键

    config.generators do |g|
      g.orm :active_record, primary_key_type: :uuid
    end
    

    此设置指示 Rails 使用 UUID 作为 ActiveRecord 模型的默认主键类型。

  2. 添加带有 UUID 的引用

    使用引用在模型之间创建关联时,请确保将数据类型指定为 :uuid,以保持与主键类型的一致性。例如

    create_table :posts, id: :uuid do |t|
      t.references :author, type: :uuid, foreign_key: true
      # Other columns...
      t.timestamps
    end
    

    在此示例中,posts 表中的 author_id 列引用 authors 表的 id 列。通过显式将类型设置为 :uuid,您可以确保外键列与它引用的主键的数据类型匹配。根据其他关联和数据库相应地调整语法。

  3. 迁移更改

    为您的模型生成迁移时,您会注意到它将 id 指定为 uuid: 类型

      $ bin/rails g migration CreateAuthors
    
    class CreateAuthors < ActiveRecord::Migration[8.1]
      def change
        create_table :authors, id: :uuid do |t|
          t.timestamps
        end
      end
    end
    

    这将导致以下 schema

    create_table "authors", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
      t.datetime "created_at", precision: 6, null: false
      t.datetime "updated_at", precision: 6, null: false
    end
    

    在此迁移中,id 列被定义为 UUID 主键,其默认值由 gen_random_uuid() 函数生成。

UUID 保证在不同系统之间全局唯一,使其适用于分布式架构。它们还通过提供不依赖于集中式 ID 生成的唯一标识符来简化与外部系统或 API 的集成,并且与自增整数不同,UUID 不会暴露有关表中记录总数的信息,这可能有利于安全目的。

但是,UUID 也可能由于其大小而影响性能,并且更难索引。与整数主键和外键相比,UUID 在写入和读取方面的性能会更差。

因此,在决定使用 UUID 作为主键之前,必须评估权衡并考虑应用程序的具体要求。

10.2. 数据迁移

数据迁移涉及在数据库中转换或移动数据。在 Rails 中,通常不建议使用迁移文件执行数据迁移。原因如下

  • 关注点分离:Schema 更改和数据更改具有不同的生命周期和目的。Schema 更改修改数据库结构,而数据更改修改内容。
  • 回滚复杂性:数据迁移很难安全且可预测地回滚。
  • 性能:数据迁移可能需要很长时间才能运行,并且可能会锁定您的表,影响应用程序性能和可用性。

相反,请考虑使用 maintenance_tasks gem。此 gem 提供了一个框架,用于以安全且易于管理的方式创建和管理数据迁移以及其他维护任务,而不会干扰 schema 迁移。



回到顶部