更多資訊請參閱 rubyonrails.org:

Active Record 遷移

遷移是 Active Record 的一項功能,可讓您隨著時間的推移演進資料庫結構。遷移並非使用純 SQL 撰寫結構修改,而是允許您使用 Ruby 領域特定語言 (DSL) 來描述表格的變更。

閱讀本指南後,您將了解

  • 您可以使用哪些產生器來建立遷移。
  • 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 之類的欄位類型來促進此過程。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 方法是最基本的遷移類型之一,但大多數時候,它會透過使用模型、資源或 scaffold 產生器為您產生。典型的用法是

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

此方法會建立一個具有名為 name 的欄位的 products 表格。

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

這會將 ENGINE=BLACKHOLE 附加到用於建立表格的 SQL 陳述式。

可以透過將 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(has and belongs to many) 連接表格。典型的用法是

create_join_table :products, :categories

此遷移將會建立一個具有兩個名為 category_idproduct_id 的欄位的 categories_products 表格。

這些欄位的選項 :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

這會將產品表格上的 part_number 欄位變更為 :text 欄位。

change_column 命令是不可逆的。為了確保您的遷移可以安全地還原,您需要提供您自己的 reversible 遷移。請參閱可還原遷移章節以取得更多詳細資訊。

除了 change_column 之外,change_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

這會將產品上的 :name 欄位設定為 NOT NULL 欄位。此變更也會套用到現有的記錄,因此您需要確保所有現有的記錄都有一個 NOT NULL:name

將 null 限制設定為 true 表示欄位將接受 null 值,否則會套用 NOT NULL 限制,並且必須傳入一個值,才能將記錄保存到資料庫。

您也可以將上述 change_column_default 遷移寫成 change_column_default :products, :approved, false,但與之前的範例不同,這會使您的遷移無法還原。

3.5 欄位修飾詞

欄位修飾詞可以在建立或變更欄位時套用

  • comment 為欄位新增註解。
  • collation 指定 stringtext 欄位的定序。
  • default 允許在欄位上設定預設值。請注意,如果您使用動態值(例如日期),則預設值只會在第一次計算(即在套用遷移的日期)。使用 nil 表示 NULL
  • 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_toadd_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 欄位作為主鍵的舊版資料庫結構描述,或者為分片或多租戶更改結構描述時,可能會出現這種情況。

您可以通過將 :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

在此範例中,我們將產品資料表的 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

此外,當執行原始 SQL 查詢或執行在 ActiveRecord 方法中沒有直接等效項的資料庫操作時,reversible 非常有用。您可以使用 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 也會確保指令以正確的順序執行。如果還原先前的範例遷移,則會在移除 users.address 欄位之後,且在刪除 distributors 資料表之前執行 down 區塊。

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 將訊息引數作為原樣輸出。可以傳遞第二個布林引數,以指定是否要縮排。
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 db:migrate 或 rails db:migrate:up 命令時,Rails 會使用此資訊來判斷需要執行哪些遷移。

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/application.rb 中定義的 config.active_record.schema_format 設定控制。預設情況下,格式為 :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 的環境,都會在名為 schema_migrations 的內部 Rails 資料庫資料表中保留對特定於它們的遷移時間戳記的參照。你可以在Rails 遷移版本控制部分中閱讀更多相關資訊。

如果你執行 bin/rails db:migrate:status 命令,它會顯示每個遷移的狀態(向上或向下),你應該會看到 ********** 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.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 提供了一個框架,用於建立和管理資料遷移和其他維護任務,其方式安全且易於管理,而不會干擾綱要遷移。



回到頂部