更多内容请访问 rubyonrails.org:

Active Record 迁移

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

阅读完本指南后,您将了解

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

1 迁移概述

迁移是一种方便的方式,可以以可重复的方式随着时间的推移发展您的数据库模式。它们使用 Ruby DSL,因此您不必手动编写 SQL,从而使您的模式和更改与数据库无关。我们建议您阅读有关 Active Record 基础Active Record 关联 的指南,以详细了解此处提到的某些概念。

您可以将每个迁移视为数据库的“新版本”。模式最初为空,每个迁移都会修改它以添加或删除表、列或索引。Active Record 知道如何在该时间线上更新您的模式,将其从历史记录中的任何位置带到最新版本。阅读更多关于 Rails 如何知道应该运行时间线上哪个迁移

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

# db/migrate/20240502100843_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.0]
  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.0].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 迁移版本控制部分 中阅读有关如何使用时间戳的更多信息。

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

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

生成器可以做更多的事情,而不是在文件名之前添加时间戳。根据命名约定和其他(可选)参数,它还可以开始填充迁移。

以下部分将介绍根据约定和其他参数创建迁移的各种方法。

2.2 创建新表

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

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

生成

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

      t.timestamps
    end
  end
end

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

2.3 添加列

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

$ bin/rails generate migration AddPartNumberToProducts part_number:string

这将生成以下迁移

class AddPartNumberToProducts < ActiveRecord::Migration[8.0]
  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.0]
  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

这将生成一个模式迁移,它将两个额外的列添加到 products 表中。

class AddDetailsToProducts < ActiveRecord::Migration[8.0]
  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.0]
  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.0]
  def change
    add_reference :products, :user, null: false, foreign_key: true
  end
end

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

  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

生成与上面相同的迁移和架构。

如果JoinTable是名称的一部分,还有一个生成器将生成联接表

$ bin/rails generate migration CreateJoinTableUserProduct user product

将生成以下迁移

class CreateJoinTableUserProduct < ActiveRecord::Migration[8.0]
  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.0]
  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.0]
  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.0]
  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文件夹中生成的迁移文件,以定义您要对数据库模式进行的进一步更改。

3.1 创建表

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

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.0]
  def change
    create_table :users, primary_key: "user_id" do |t|
      t.string :username
      t.string :email
      t.timestamps
    end
  end
end

这将产生以下架构

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.0]
  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.0]
  def change
    create_table :users, id: false do |t|
      t.string :username
      t.string :email
      t.timestamps
    end
  end
end

3.1.3 数据库选项

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

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.0]
  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(多对多)联接表。典型的用法是

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_column之外,change_column_nullchange_column_default方法用于更改列的空约束和默认值。

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

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

change_column_null :products, :name, false

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

将空约束设置为true意味着该列将接受空值,否则将应用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值。
  • precisiondecimal/numeric/datetime/time列指定精度。
  • scaledecimalnumeric列指定比例,表示小数点后的位数。

对于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_toadd_reference的别名。

add_belongs_to :taggings, :taggable, polymorphic: true

多态选项将在标签表上创建两列,可用于多态关联: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 相匹配,以确保文章表中列出的所有审阅者都是作者表中列出的有效作者。

在迁移中使用 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 需要使用复合外键。请参阅 架构转储与您

3.8 复合主键

有时单列的值不足以唯一地标识表的每一行,但两个或多个列的组合确实唯一地标识它。当使用没有单个 id 列作为主键的遗留数据库架构时,或者当更改用于分片或多租户的架构时,情况可能如此。

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

class CreateProducts < ActiveRecord::Migration[8.0]
  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.0]
  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.0]
  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.0]
  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.0]
  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 将确保指令以正确的顺序执行。如果还原上一个示例迁移,down 块将在删除 users.address 列后运行,并且在删除 distributors 表之前运行。

3.12 使用 up/down 方法

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

up 方法应该描述您希望对架构进行的转换,而迁移的 down 方法应该还原 up 方法完成的转换。换句话说,如果您执行 up 后跟 down,数据库架构应该保持不变。

例如,如果您在 up 方法中创建一个表,则应该在 down 方法中删除它。明智的做法是按 up 方法中完成的顺序完全相反的顺序执行转换。reversible 部分中的示例等效于

class ExampleMigration < ActiveRecord::Migration[8.0]
  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.0]
  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.0]
  def change
    revert ExampleMigration

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

revert 方法也接受一组用于反转的指令。这可能对还原以前迁移的选定部分很有用。

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

class DontUseDistributorsViewMigration < ActiveRecord::Migration[8.0]
  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 将运行所需的迁移(change、up、down),直到它达到指定的版本。 版本是迁移文件名上的数字前缀。 例如,要迁移到版本 20240428000000,请运行

$ bin/rails db:migrate VERSION=20240428000000

如果版本 20240428000000 大于当前版本(即向上迁移),这将运行 change(或 up)方法对所有迁移,包括 20240428000000,并且不会执行任何更晚的迁移。 如果向下迁移,这将运行 down 方法对所有迁移,直到但不包括 20240428000000。

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 事务的数据库中,在单个事务中更改模式,每个迁移都包含在一个事务中。

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

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

您不能在事务内执行某些查询,对于这些情况,您可以使用 disable_ddl_transaction! 关闭自动事务。

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

4.2 设置数据库

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

4.3 准备数据库

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

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

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

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

4.4 重置数据库

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

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

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_with_time 输出文本以及运行其块所花费的时间。 如果块返回一个整数,它假定它是受影响的行数。

例如,取以下迁移

class CreateProducts < ActiveRecord::Migration[8.0]
  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 控制台查看 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 模式转储和您

6.1 模式文件是做什么用的?

迁移可能很强大,但它们不是您数据库模式的权威来源。 **您的数据库仍然是真相的来源。**

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

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

模式文件还有助于您快速了解 Active Record 对象具有哪些属性。 此信息不在模型代码中,并且经常分布在多个迁移中,但是该信息在模式文件中很好地总结。

6.2 模式转储类型

Rails 生成的模式转储格式由 config.active_record.schema_format 设置控制,该设置在 config/application.rb 中定义。 默认情况下,格式为 :ruby,或者可以设置为 :sql

6.2.1 使用默认的 :ruby 模式

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

ActiveRecord::Schema[8.0].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 模式转储器

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

虽然迁移可以使用 execute 来创建 Ruby 迁移 DSL 不支持的数据库结构,但这些结构可能无法通过模式转储器重建。

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

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

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

6.3 模式转储和源代码管理

由于模式文件通常用于创建新数据库,因此强烈建议您将模式文件检入源代码管理。

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

新生成的 Rails 应用程序将已经包含在 git 树中的迁移文件夹,因此您只需确保添加您添加的任何新迁移并提交它们。

7 Active Record 和参照完整性

Active Record 模式建议智能主要应该驻留在您的模型中,而不是在数据库中。因此,诸如触发器或约束之类的功能(将部分智能委托回数据库)并不总是受欢迎。

诸如validates :foreign_key, uniqueness: true之类的验证是模型可以强制执行数据完整性的方法之一。关联上的:dependent选项允许模型在父级被销毁时自动销毁子对象。与在应用程序级别运行的任何事物一样,这些无法保证参照完整性,因此有些人用数据库中的外键约束来增强它们。

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

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

8 迁移和种子数据

Rails 迁移功能的主要目的是发出命令以使用一致的过程修改模式。迁移也可以用来添加或修改数据。这在无法销毁和重新创建的现有数据库(例如生产数据库)中很有用。

class AddInitialProducts < ActiveRecord::Migration[8.0]
  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命令,该命令显示每个迁移的状态(向上或向下),您应该在曾经在特定环境上执行但现在在db/migrate/目录中找不到的任何已删除的迁移文件旁边看到********** NO FILE **********

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。

如果您使用的是 13 之前的 PostgreSQL 版本,您可能仍然需要启用 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.0]
      def change
        create_table :authors, id: :uuid do |t|
          t.timestamps
        end
      end
    end
    

    这将导致以下模式

    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 中,通常不建议使用迁移文件执行数据迁移。以下是原因

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

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



返回顶部