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_at
和 updated_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_column
和 add_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_to
是references
的别名,因此上述内容可以改写为
$ 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
生成器之外,model
、resource
和scaffold
生成器将创建适合添加新模型的迁移。此迁移将已经包含用于创建相关表的指令。如果您告诉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 --help
或bin/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_id
、taggable_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_id
和product_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
此迁移将删除description
和name
列,创建一个名为part_number
的新字符串列,并在其上添加索引。最后,它将upccode
列重命名为upc_code
。
3.4 更改列
与我们之前介绍的remove_column
和add_column
方法类似,Rails 还提供了change_column
迁移方法。
change_column :products, :part_number, :text
这将products表上的part_number
列更改为:text
字段。
change_column
命令是不可逆的。要确保您的迁移可以安全地回滚,您需要提供自己的reversible
迁移。有关更多详细信息,请参阅可逆迁移部分。
除了change_column
之外,change_column_null
和change_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
为列添加注释。collation
为string
或text
列指定排序规则。default
允许在列上设置默认值。请注意,如果您使用的是动态值(例如日期),则默认值仅在第一次(即在应用迁移的日期)计算。对NULL
使用nil
。limit
设置string
列的最大字符数以及text/binary/integer
列的最大字节数。null
允许或不允许列中的NULL
值。precision
为decimal/numeric/datetime/time
列指定精度。scale
为decimal
和numeric
列指定比例,表示小数点后的位数。
对于add_column
或change_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
多态选项将在标签表上创建两列,可用于多态关联:taggable_type
和 taggable_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
支持几个其他选项,例如 name
、on_delete
、if_not_exists
、validate
和 deferrable
。
也可以使用 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 仅支持单列外键。execute
和 structure.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
的文档,它提供了在 change
、up
和 down
方法中可用的方法。
有关 create_table
生成的对象的可用方法,请参阅 ActiveRecord::ConnectionAdapters::TableDefinition
。
有关 change_table
生成的对象的可用方法,请参阅 ActiveRecord::ConnectionAdapters::Table
。
3.10 使用 change
方法
change
方法是编写迁移的主要方式。它适用于 Active Record 知道如何自动反转迁移操作的大多数情况。以下是 change
支持的一些操作。
add_check_constraint
add_column
add_foreign_key
add_index
add_reference
add_timestamps
change_column_comment
(必须提供:from
和:to
选项)change_column_default
(必须提供:from
和:to
选项)change_column_null
change_table_comment
(必须提供:from
和:to
选项)create_join_table
create_table
disable_extension
drop_join_table
drop_table
(必须提供表创建选项和块)enable_extension
remove_check_constraint
(必须提供原始约束表达式)remove_column
(必须提供原始类型和列选项)remove_columns
(必须提供原始类型和列选项)remove_foreign_key
(必须提供其他表和原始选项)remove_index
(必须提供列和原始选项)remove_reference
(必须提供原始选项)remove_timestamps
(必须提供原始选项)rename_column
rename_index
rename_table
change_table
也是可逆的,只要块只调用上面列出的可逆操作。
如果您需要使用任何其他方法,则应使用 reversible
或编写 up
和 down
方法,而不是使用 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.up
和 direction.down
的块。
或者,您可以使用 up
和 down
而不是 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
方法
您也可以使用旧的迁移样式,使用 up
和 down
方法而不是 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
的情况下编写,但这将涉及更多步骤
- 反转
create_table
和reversible
的顺序。 - 将
create_table
替换为drop_table
。 - 最后,将
up
替换为down
,反之亦然。
所有这些都由 revert
处理。
4 运行迁移
Rails 提供了一组命令来运行特定的迁移集。
您将使用的第一个与迁移相关的 Rails 命令可能是 bin/rails db:migrate
。 在最基本的形式中,它只是为所有尚未运行的迁移运行 change
或 up
方法。 如果没有这样的迁移,它将退出。 它将根据迁移的日期按顺序运行这些迁移。
请注意,运行 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.rb
或 db/structure.sql
文件的内容。 如果无法回滚迁移,bin/rails db:reset
可能无法帮助您。 要了解有关转储模式的更多信息,请参见 模式转储和您 部分。
4.5 运行特定迁移
如果您需要向上或向下运行特定迁移,db:migrate:up
和 db:migrate:down
命令将执行此操作。 只需指定相应的版本,相应的迁移将调用其 change
、up
或 down
方法,例如
$ 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_table
、add_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.rb
或db/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()
函数。
Rails 配置
在您的 Rails 应用程序配置文件 (
config/application.rb
) 中,添加以下行以配置 Rails 默认生成 UUID 作为主键config.generators do |g| g.orm :active_record, primary_key_type: :uuid end
此设置指示 Rails 使用 UUID 作为 ActiveRecord 模型的默认主键类型。
添加带有 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
,您确保外键列与它引用的主键的数据类型匹配。相应地调整其他关联和数据库的语法。迁移更改
当为您的模型生成迁移时,您会注意到它指定 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 提供了一个框架,用于以安全且易于管理的方式创建和管理数据迁移和其他维护任务,而不会干扰模式迁移。