更多資訊請參考 rubyonrails.org:

Active Record 查詢介面

本指南涵蓋使用 Active Record 從資料庫擷取資料的不同方式。

閱讀本指南後,您將了解

  • 如何使用各種方法和條件尋找記錄。
  • 如何指定找到的記錄的順序、擷取的屬性、分組和其他屬性。
  • 如何使用預先載入來減少資料擷取所需的資料庫查詢次數。
  • 如何使用動態尋找器方法。
  • 如何使用方法鏈結將多個 Active Record 方法一起使用。
  • 如何檢查特定記錄是否存在。
  • 如何在 Active Record 模型上執行各種計算。
  • 如何在關聯上執行 EXPLAIN。

1 什麼是 Active Record 查詢介面?

如果您習慣使用原始 SQL 來尋找資料庫記錄,那麼您通常會發現有更好的方法可以在 Rails 中執行相同的操作。在大多數情況下,Active Record 可以讓您不必使用 SQL。

Active Record 會為您執行資料庫查詢,並且與大多數資料庫系統相容,包括 MySQL、MariaDB、PostgreSQL 和 SQLite。無論您使用哪個資料庫系統,Active Record 方法格式始終相同。

本指南中的程式碼範例將參考以下一個或多個模型

除非另有說明,否則以下所有模型都使用 id 作為主鍵。

class Author < ApplicationRecord
  has_many :books, -> { order(year_published: :desc) }
end
class Book < ApplicationRecord
  belongs_to :supplier
  belongs_to :author
  has_many :reviews
  has_and_belongs_to_many :orders, join_table: "books_orders"

  scope :in_print, -> { where(out_of_print: false) }
  scope :out_of_print, -> { where(out_of_print: true) }
  scope :old, -> { where(year_published: ...50.years.ago.year) }
  scope :out_of_print_and_expensive, -> { out_of_print.where("price > 500") }
  scope :costs_more_than, ->(amount) { where("price > ?", amount) }
end
class Customer < ApplicationRecord
  has_many :orders
  has_many :reviews
end
class Order < ApplicationRecord
  belongs_to :customer
  has_and_belongs_to_many :books, join_table: "books_orders"

  enum :status, [:shipped, :being_packed, :complete, :cancelled]

  scope :created_before, ->(time) { where(created_at: ...time) }
end
class Review < ApplicationRecord
  belongs_to :customer
  belongs_to :book

  enum :state, [:not_reviewed, :published, :hidden]
end
class Supplier < ApplicationRecord
  has_many :books
  has_many :authors, through: :books
end

Diagram of all of the bookstore models

2 從資料庫擷取物件

為了從資料庫擷取物件,Active Record 提供了多種尋找器方法。每個尋找器方法都允許您傳遞參數來對您的資料庫執行某些查詢,而無需編寫原始 SQL。

這些方法包括

傳回集合的尋找器方法,例如 wheregroup,會傳回 ActiveRecord::Relation 的執行個體。尋找單一實體的方法,例如 findfirst,會傳回模型的單一執行個體。

Model.find(options) 的主要操作可以總結為

  • 將提供的選項轉換為等效的 SQL 查詢。
  • 執行 SQL 查詢並從資料庫擷取對應的結果。
  • 為每個結果列實例化適當模型的等效 Ruby 物件。
  • 執行 after_find 然後執行 after_initialize 回呼 (如果有)。

2.1 擷取單一物件

Active Record 提供了幾種不同的方式來擷取單一物件。

2.1.1 find

使用 find 方法,您可以擷取符合任何提供的選項的指定主鍵的物件。例如

# Find the customer with primary key (id) 10.
irb> customer = Customer.find(10)
=> #<Customer id: 10, first_name: "Ryan">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers WHERE (customers.id = 10) LIMIT 1

如果找不到符合的記錄,find 方法將引發 ActiveRecord::RecordNotFound 例外。

您也可以使用此方法查詢多個物件。呼叫 find 方法並傳入主鍵陣列。傳回值將是一個包含所有符合提供的主鍵的記錄的陣列。例如

# Find the customers with primary keys 1 and 10.
irb> customers = Customer.find([1, 10]) # OR Customer.find(1, 10)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 10, first_name: "Ryan">]

以上述程式碼等效的 SQL 如下

SELECT * FROM customers WHERE (customers.id IN (1,10))

除非找到符合所有提供的主鍵的記錄,否則 find 方法將引發 ActiveRecord::RecordNotFound 例外。

如果您的表格使用複合主鍵,您需要傳遞一個陣列給 find 來尋找單一項目。例如,如果客戶定義了 [:store_id, :id] 作為主鍵

# Find the customer with store_id 3 and id 17
irb> customers = Customer.find([3, 17])
=> #<Customer store_id: 3, id: 17, first_name: "Magda">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers WHERE store_id = 3 AND id = 17

若要尋找具有複合 ID 的多個客戶,您需要傳遞陣列的陣列

# Find the customers with primary keys [1, 8] and [7, 15].
irb> customers = Customer.find([[1, 8], [7, 15]]) # OR Customer.find([1, 8], [7, 15])
=> [#<Customer store_id: 1, id: 8, first_name: "Pat">, #<Customer store_id: 7, id: 15, first_name: "Chris">]

以上述程式碼等效的 SQL 如下

SELECT * FROM customers WHERE (store_id = 1 AND id = 8 OR store_id = 7 AND id = 15)

2.1.2 take

take 方法擷取記錄,而不帶任何隱含排序。例如

irb> customer = Customer.take
=> #<Customer id: 1, first_name: "Lifo">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers LIMIT 1

如果找不到記錄,take 方法會傳回 nil,且不會引發例外。

您可以傳遞一個數值引數給 take 方法,以傳回最多該數量的結果。例如

irb> customers = Customer.take(2)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 220, first_name: "Sara">]

以上述程式碼等效的 SQL 如下

SELECT * FROM customers LIMIT 2

take! 方法的行為與 take 完全相同,只是如果找不到符合的記錄,它會引發 ActiveRecord::RecordNotFound

擷取的記錄可能會因資料庫引擎而異。

2.1.3 first

first 方法會尋找按主鍵 (預設) 排序的第一個記錄。例如

irb> customer = Customer.first
=> #<Customer id: 1, first_name: "Lifo">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers ORDER BY customers.id ASC LIMIT 1

如果找不到符合的記錄,first 方法會傳回 nil,且不會引發例外。

如果您的 預設範圍包含 order 方法,則 first 將根據此排序傳回第一個記錄。

您可以傳遞一個數值引數給 first 方法,以傳回最多該數量的結果。例如

irb> customers = Customer.first(3)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 2, first_name: "Fifo">, #<Customer id: 3, first_name: "Filo">]

以上述程式碼等效的 SQL 如下

SELECT * FROM customers ORDER BY customers.id ASC LIMIT 3

具有複合主鍵的模型會使用完整複合主鍵進行排序。例如,如果客戶的主鍵定義為 [:store_id, :id]

irb> customer = Customer.first
=> #<Customer id: 2, store_id: 1, first_name: "Lifo">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers ORDER BY customers.store_id ASC, customers.id ASC LIMIT 1

在使用 order 排序的集合中,first 會回傳依照 order 指定屬性排序的第一筆記錄。

irb> customer = Customer.order(:first_name).first
=> #<Customer id: 2, first_name: "Fifo">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers ORDER BY customers.first_name ASC LIMIT 1

first! 方法的行為與 first 完全相同,唯一的差別在於如果找不到符合的記錄,它會引發 ActiveRecord::RecordNotFound 例外。

2.1.4 last

last 方法會找出依主鍵(預設)排序的最後一筆記錄。例如:

irb> customer = Customer.last
=> #<Customer id: 221, first_name: "Russel">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers ORDER BY customers.id DESC LIMIT 1

如果找不到符合的記錄,last 方法會回傳 nil,且不會引發任何例外。

具有複合主鍵的模型會使用完整複合主鍵進行排序。例如,如果客戶的主鍵定義為 [:store_id, :id]

irb> customer = Customer.last
=> #<Customer id: 221, store_id: 1, first_name: "Lifo">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers ORDER BY customers.store_id DESC, customers.id DESC LIMIT 1

如果您的預設範圍包含 order 方法,last 會根據此排序回傳最後一筆記錄。

您可以將數值引數傳遞給 last 方法,以回傳最多該數量的結果。例如:

irb> customers = Customer.last(3)
=> [#<Customer id: 219, first_name: "James">, #<Customer id: 220, first_name: "Sara">, #<Customer id: 221, first_name: "Russel">]

以上述程式碼等效的 SQL 如下

SELECT * FROM customers ORDER BY customers.id DESC LIMIT 3

在使用 order 排序的集合中,last 會回傳依照 order 指定屬性排序的最後一筆記錄。

irb> customer = Customer.order(:first_name).last
=> #<Customer id: 220, first_name: "Sara">

以上述程式碼等效的 SQL 如下

SELECT * FROM customers ORDER BY customers.first_name DESC LIMIT 1

last! 方法的行為與 last 完全相同,唯一的差別在於如果找不到符合的記錄,它會引發 ActiveRecord::RecordNotFound 例外。

2.1.5 find_by

find_by 方法會找出符合某些條件的第一筆記錄。例如:

irb> Customer.find_by first_name: 'Lifo'
=> #<Customer id: 1, first_name: "Lifo">

irb> Customer.find_by first_name: 'Jon'
=> nil

這等同於寫成:

Customer.where(first_name: "Lifo").take

以上述程式碼等效的 SQL 如下

SELECT * FROM customers WHERE (customers.first_name = 'Lifo') LIMIT 1

請注意,上述 SQL 中沒有 ORDER BY。如果您的 find_by 條件可以符合多筆記錄,您應該套用排序以確保結果具有確定性。

find_by! 方法的行為與 find_by 完全相同,唯一的差別在於如果找不到符合的記錄,它會引發 ActiveRecord::RecordNotFound 例外。例如:

irb> Customer.find_by! first_name: 'does not exist'
ActiveRecord::RecordNotFound

這等同於寫成:

Customer.where(first_name: "does not exist").take!
2.1.5.1 帶有 :id 的條件

當在 find_bywhere 等方法中指定條件時,使用 id 會比對模型上的 :id 屬性。這與 find 不同,在 find 中傳入的 ID 應該是主鍵值。

:id 不是主鍵的模型上(例如複合主鍵模型)使用 find_by(id:) 時要小心。例如,如果客戶的主鍵定義為 [:store_id, :id]

irb> customer = Customer.last
=> #<Customer id: 10, store_id: 5, first_name: "Joe">
irb> Customer.find_by(id: customer.id) # Customer.find_by(id: [5, 10])
=> #<Customer id: 5, store_id: 3, first_name: "Bob">

這裡,我們可能打算搜尋具有複合主鍵 [5, 10] 的單一記錄,但 Active Record 會搜尋 :id 欄位值為 5 或 10 的記錄,並可能回傳錯誤的記錄。

id_value 方法可以用於取得記錄的 :id 欄位值,以供 find_bywhere 等尋找方法使用。請參閱以下範例:

irb> customer = Customer.last
=> #<Customer id: 10, store_id: 5, first_name: "Joe">
irb> Customer.find_by(id: customer.id_value) # Customer.find_by(id: 10)
=> #<Customer id: 10, store_id: 5, first_name: "Joe">

2.2 批次擷取多個物件

我們經常需要迭代處理大量的記錄,例如當我們向大量客戶發送電子報,或匯出資料時。

這看起來可能很簡單

# This may consume too much memory if the table is big.
Customer.all.each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

但隨著表格大小增加,這種方法會變得越來越不切實際,因為 Customer.all.each 指示 Active Record 一次性擷取整個表格,為每一列建立模型物件,然後將整個模型物件陣列保留在記憶體中。實際上,如果我們有大量的記錄,整個集合可能會超過可用的記憶體量。

Rails 提供了兩種方法來解決這個問題,將記錄分割成記憶體友善的批次以進行處理。第一種方法 find_each 會擷取一批記錄,然後將每個記錄個別作為模型傳遞給區塊。第二種方法 find_in_batches 會擷取一批記錄,然後將整個批次作為模型陣列傳遞給區塊。

find_eachfind_in_batches 方法適用於批次處理大量無法一次全部放入記憶體的記錄。如果您只需要迴圈處理一千筆記錄,則正規的尋找方法是較佳的選擇。

2.2.1 find_each

find_each 方法會批次擷取記錄,然後將每一個記錄傳遞給區塊。在以下範例中,find_each 會以 1000 筆為單位批次擷取客戶,並將它們逐一傳遞給區塊

Customer.find_each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

這個程序會重複執行,視需要擷取更多批次,直到所有記錄都處理完畢。

find_each 可用於模型類別,如上所示,也可用於關聯

Customer.where(weekly_subscriber: true).find_each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

只要它們沒有排序,因為該方法需要在內部強制執行排序才能迭代。

如果接收器中存在排序,則行為取決於 config.active_record.error_on_ignored_order 旗標。如果為 true,則會引發 ArgumentError,否則會忽略排序並發出警告,這是預設行為。這可以使用 :error_on_ignore 選項覆寫,詳情如下。

2.2.1.1 find_each 的選項

:batch_size

:batch_size 選項允許您指定在將記錄個別傳遞給區塊之前,要在每個批次中擷取的記錄數。例如,若要擷取 5000 筆為一批的記錄:

Customer.find_each(batch_size: 5000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

:start

預設情況下,記錄會依主鍵的遞增順序擷取。當您需要的第一個 ID 不是最小的 ID 時,:start 選項允許您設定序列的第一個 ID。舉例來說,如果您想恢復中斷的批次程序,只要您將最後處理的 ID 儲存為檢查點,這會很有用。

例如,若要僅向主鍵從 2000 開始的客戶發送電子報:

Customer.find_each(start: 2000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

:finish

:start 選項類似,當您需要的最高 ID 不是最大的 ID 時,:finish 允許您設定序列的最後一個 ID。舉例來說,如果您想使用基於 :start:finish 的記錄子集來執行批次程序,這會很有用。

例如,若要僅向主鍵從 2000 到 10000 的客戶發送電子報:

Customer.find_each(start: 2000, finish: 10000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

另一個例子是,如果您想要多個工作者處理相同的處理佇列。您可以透過在每個工作者上設定適當的 :start:finish 選項,讓每個工作者處理 10000 筆記錄。

:error_on_ignore

覆寫應用程式設定,以指定當關聯中存在排序時是否應引發錯誤。

:order

指定主鍵排序(可以是 :asc:desc)。預設為 :asc

Customer.find_each(order: :desc) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

2.2.2 find_in_batches

find_in_batches 方法與 find_each 類似,因為兩者都會擷取批次的記錄。不同之處在於,find_in_batches 會將批次作為模型陣列傳遞給區塊,而不是個別傳遞。以下範例會將最多 1000 個客戶的陣列一次傳遞給提供的區塊,最後一個區塊會包含任何剩餘的客戶

# Give add_customers an array of 1000 customers at a time.
Customer.find_in_batches do |customers|
  export.add_customers(customers)
end

find_in_batches 可用於模型類別,如上所示,也可用於關聯

# Give add_customers an array of 1000 recently active customers at a time.
Customer.recently_active.find_in_batches do |customers|
  export.add_customers(customers)
end

只要它們沒有排序,因為該方法需要在內部強制執行排序才能迭代。

2.2.2.1 find_in_batches 的選項

find_in_batches 方法接受與 find_each 相同的選項

:batch_size

如同 find_eachbatch_size 建立每個群組將擷取的記錄數。例如,擷取 2500 筆記錄為一批可以指定為:

Customer.find_in_batches(batch_size: 2500) do |customers|
  export.add_customers(customers)
end

:start

start 選項允許指定將從哪裡選取記錄的起始 ID。如前所述,預設情況下,記錄會依主鍵的遞增順序擷取。例如,若要從 ID:5000 開始以 2500 筆記錄為一批擷取客戶,可以使用以下程式碼:

Customer.find_in_batches(batch_size: 2500, start: 5000) do |customers|
  export.add_customers(customers)
end

:finish

finish 選項允許指定要擷取的記錄的結束 ID。以下程式碼顯示了以批次擷取客戶的案例,直到 ID:7000 的客戶為止:

Customer.find_in_batches(finish: 7000) do |customers|
  export.add_customers(customers)
end

:error_on_ignore

error_on_ignore 選項會覆寫應用程式設定,以指定當關聯中存在特定排序時是否應引發錯誤。

3 條件

where 方法允許您指定條件來限制回傳的記錄,代表 SQL 陳述式的 WHERE 部分。條件可以指定為字串、陣列或雜湊。

3.1 純字串條件

如果您想在尋找中加入條件,可以直接在其中指定,就像 Book.where("title = 'Introduction to Algorithms'") 一樣。這會找到 title 欄位值為 'Introduction to Algorithms' 的所有書籍。

將您自己的條件建構為純字串可能會使您容易受到 SQL 注入攻擊。例如,Book.where("title LIKE '%#{params[:title]}%'") 是不安全的。請參閱下一節,了解使用陣列處理條件的較佳方式。

3.2 陣列條件

現在,如果標題可能會變動,例如來自某個地方的引數呢?那麼尋找將採用以下形式:

Book.where("title = ?", params[:title])

Active Record 會將第一個引數作為條件字串,並且任何其他引數都會取代其中的問號 (?)

如果您想要指定多個條件:

Book.where("title = ? AND out_of_print = ?", params[:title], false)

在此範例中,第一個問號將被 params[:title] 中的值取代,第二個問號將被 false 的 SQL 表示法取代,這取決於轉接器。

這個程式碼是高度較佳的:

Book.where("title = ?", params[:title])

而不是這個程式碼:

Book.where("title = #{params[:title]}")

因為引數的安全性。將變數直接放入條件字串中,會將變數按原樣傳遞給資料庫。這表示它會是來自可能懷有惡意的使用者未轉義的變數。如果您這樣做,您會將整個資料庫置於風險中,因為一旦使用者發現他們可以利用您的資料庫,他們幾乎可以對它做任何事情。永遠不要將您的引數直接放入條件字串中。

關於 SQL 注入的危險性,請參閱 Ruby on Rails 安全指南

3.2.1 佔位符條件

類似於參數的 (?) 取代樣式,您也可以在條件字串中指定鍵,並搭配相應的鍵/值雜湊。

Book.where("created_at >= :start_date AND created_at <= :end_date",
  { start_date: params[:start_date], end_date: params[:end_date] })

如果您有大量的變數條件,這可以提高程式碼的可讀性。

3.2.2 使用 LIKE 的條件

雖然條件參數會自動逸出以防止 SQL 注入,但 SQL 的 LIKE 萬用字元(即 %_不會逸出。如果參數中使用未經處理的值,可能會導致意外的行為。例如:

Book.where("title LIKE ?", params[:title] + "%")

在上面的程式碼中,其目的是匹配以使用者指定的字串開頭的標題。但是,params[:title] 中任何出現的 %_ 都會被視為萬用字元,導致令人驚訝的查詢結果。在某些情況下,這也可能會阻止資料庫使用預期的索引,導致查詢速度慢很多。

為了避免這些問題,請使用 sanitize_sql_like 來逸出參數相關部分中的萬用字元。

Book.where("title LIKE ?",
  Book.sanitize_sql_like(params[:title]) + "%")

3.3 雜湊條件

Active Record 也允許您傳遞雜湊條件,這可以提高條件語法的可讀性。使用雜湊條件時,您會傳遞一個雜湊,其中包含您要限定的欄位作為鍵,以及您要如何限定它們的值。

雜湊條件僅可進行相等性、範圍和子集檢查。

3.3.1 相等條件

Book.where(out_of_print: true)

這會產生如下的 SQL:

SELECT * FROM books WHERE (books.out_of_print = 1)

欄位名稱也可以是字串。

Book.where("out_of_print" => true)

在屬於 (belongs_to) 關係的情況下,如果將 Active Record 物件用作值,則可以使用關聯鍵來指定模型。此方法也適用於多型關係。

author = Author.first
Book.where(author: author)
Author.joins(:books).where(books: { author: author })

雜湊條件也可以使用類似元組的語法指定,其中鍵是欄位的陣列,而值是元組的陣列。

Book.where([:author_id, :id] => [[15, 1], [15, 2]])

這種語法對於查詢使用複合主鍵的資料表關係非常有用。

class Book < ApplicationRecord
  self.primary_key = [:author_id, :id]
end

Book.where(Book.primary_key => [[2, 1], [3, 1]])

3.3.2 範圍條件

Book.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)

這將使用 BETWEEN SQL 語句來尋找昨天建立的所有書籍。

SELECT * FROM books WHERE (books.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')

這示範了 陣列條件 中的範例的較短語法。

支援無始和無終範圍,可用於建立小於/大於條件。

Book.where(created_at: (Time.now.midnight - 1.day)..)

這會產生如下的 SQL:

SELECT * FROM books WHERE books.created_at >= '2008-12-21 00:00:00'

3.3.3 子集條件

如果您想使用 IN 運算式來尋找記錄,您可以將陣列傳遞給條件雜湊。

Customer.where(orders_count: [1, 3, 5])

此程式碼會產生如下的 SQL:

SELECT * FROM customers WHERE (customers.orders_count IN (1,3,5))

3.4 NOT 條件

可以使用 where.not 來建立 NOT SQL 查詢。

Customer.where.not(orders_count: [1, 3, 5])

換句話說,可以透過呼叫沒有引數的 where,然後立即以 not 鏈接並傳遞 where 條件來產生此查詢。這會產生如下的 SQL:

SELECT * FROM customers WHERE (customers.orders_count NOT IN (1,3,5))

如果查詢在可為空值的欄位上具有非空值雜湊條件,則不會傳回在可為空值欄位上具有 nil 值的記錄。例如:

Customer.create!(nullable_country: nil)
Customer.where.not(nullable_country: "UK")
# => []

# But
Customer.create!(nullable_country: "UK")
Customer.where.not(nullable_country: nil)
# => [#<Customer id: 2, nullable_country: "UK">]

3.5 OR 條件

兩個關係之間的 OR 條件可以透過在第一個關係上呼叫 or,並將第二個關係作為引數傳遞來建立。

Customer.where(last_name: "Smith").or(Customer.where(orders_count: [1, 3, 5]))
SELECT * FROM customers WHERE (customers.last_name = 'Smith' OR customers.orders_count IN (1,3,5))

3.6 AND 條件

可以透過鏈接 where 條件來建立 AND 條件。

Customer.where(last_name: "Smith").where(orders_count: [1, 3, 5])
SELECT * FROM customers WHERE customers.last_name = 'Smith' AND customers.orders_count IN (1,3,5)

關係之間邏輯交集的 AND 條件可以透過在第一個關係上呼叫 and,並將第二個關係作為引數傳遞來建立。

Customer.where(id: [1, 2]).and(Customer.where(id: [2, 3]))
SELECT * FROM customers WHERE (customers.id IN (1, 2) AND customers.id IN (2, 3))

4 排序

若要以特定順序從資料庫擷取記錄,您可以使用 order 方法。

例如,如果您要取得一組記錄,並且想要按資料表中 created_at 欄位的遞增順序排序它們:

Book.order(:created_at)
# OR
Book.order("created_at")

您也可以指定 ASCDESC

Book.order(created_at: :desc)
# OR
Book.order(created_at: :asc)
# OR
Book.order("created_at DESC")
# OR
Book.order("created_at ASC")

或按多個欄位排序。

Book.order(title: :asc, created_at: :desc)
# OR
Book.order(:title, created_at: :desc)
# OR
Book.order("title ASC, created_at DESC")
# OR
Book.order("title ASC", "created_at DESC")

如果您想要多次呼叫 order,後續的排序將附加到第一個。

irb> Book.order("title ASC").order("created_at DESC")
SELECT * FROM books ORDER BY title ASC, created_at DESC

您也可以從聯結的資料表排序。

Book.includes(:author).order(books: { print_year: :desc }, authors: { name: :asc })
# OR
Book.includes(:author).order("books.print_year desc", "authors.name asc")

在大多數資料庫系統中,當使用 selectpluckids 等方法從結果集中選取具有 distinct 的欄位時;除非 order 子句中使用的欄位包含在選取清單中,否則 order 方法會引發 ActiveRecord::StatementInvalid 例外。請參閱下一節,瞭解如何從結果集中選取欄位。

5 選取特定欄位

預設情況下,Model.find 會使用 select * 從結果集中選取所有欄位。

若要僅從結果集中選取欄位的子集,您可以使用 select 方法指定子集。

例如,若要僅選取 isbnout_of_print 欄位:

Book.select(:isbn, :out_of_print)
# OR
Book.select("isbn, out_of_print")

此尋找呼叫使用的 SQL 查詢會類似於:

SELECT isbn, out_of_print FROM books

請小心,因為這也表示您僅使用您選取的欄位初始化模型物件。如果您嘗試存取未在已初始化的記錄中的欄位,您會收到:

ActiveModel::MissingAttributeError: missing attribute '<attribute>' for Book

其中 <attribute> 是您要求的屬性。id 方法不會引發 ActiveRecord::MissingAttributeError,因此在處理關聯時請務必小心,因為它們需要 id 方法才能正常運作。

如果您只想抓取特定欄位中每個唯一值的一筆記錄,您可以使用 distinct

Customer.select(:last_name).distinct

這會產生如下的 SQL:

SELECT DISTINCT last_name FROM customers

您也可以移除唯一性限制。

# Returns unique last_names
query = Customer.select(:last_name).distinct

# Returns all last_names, even if there are duplicates
query.distinct(false)

6 Limit 和 Offset

若要將 LIMIT 套用至 Model.find 所觸發的 SQL,您可以使用關係上的 limitoffset 方法指定 LIMIT

您可以使用 limit 指定要擷取的記錄數,並使用 offset 指定在開始傳回記錄之前要跳過的記錄數。例如:

Customer.limit(5)

將會傳回最多 5 個客戶,並且因為它沒有指定偏移量,因此將傳回資料表中的前 5 個。它執行的 SQL 如下所示:

SELECT * FROM customers LIMIT 5

在其中新增 offset

Customer.limit(5).offset(30)

則會改為傳回最多 5 個客戶,從第 31 個開始。SQL 如下所示:

SELECT * FROM customers LIMIT 5 OFFSET 30

7 分組

若要將 GROUP BY 子句套用至 Finder 觸發的 SQL,您可以使用 group 方法。

例如,如果您想要尋找建立訂單的日期集合:

Order.select("created_at").group("created_at")

這將為您提供資料庫中存在訂單的每個日期的單個 Order 物件。

將執行的 SQL 類似於:

SELECT created_at
FROM orders
GROUP BY created_at

7.1 分組項目的總數

若要取得單一查詢中分組項目的總數,請在 group 之後呼叫 count

irb> Order.group(:status).count
=> {"being_packed"=>7, "shipped"=>12}

將執行的 SQL 類似於:

SELECT COUNT (*) AS count_all, status AS status
FROM orders
GROUP BY status

7.2 HAVING 條件

SQL 使用 HAVING 子句來指定 GROUP BY 欄位的條件。您可以將 HAVING 子句新增至 Model.find 觸發的 SQL,方法是將 having 方法新增至尋找。

例如:

Order.select("created_at as ordered_date, sum(total) as total_price").
  group("created_at").having("sum(total) > ?", 200)

將執行的 SQL 類似於:

SELECT created_at as ordered_date, sum(total) as total_price
FROM orders
GROUP BY created_at
HAVING sum(total) > 200

這會傳回每個訂單物件的日期和總價,這些訂單物件會依訂購日期分組,且總價超過 200 美元。

您可以像這樣存取傳回的每個訂單物件的 total_price

big_orders = Order.select("created_at, sum(total) as total_price")
                  .group("created_at")
                  .having("sum(total) > ?", 200)

big_orders[0].total_price
# Returns the total price for the first Order object

8 覆寫條件

8.1 unscope

您可以使用 unscope 方法指定要移除的某些條件。例如:

Book.where("id > 100").limit(20).order("id desc").unscope(:order)

將執行的 SQL:

SELECT * FROM books WHERE id > 100 LIMIT 20

-- Original query without `unscope`
SELECT * FROM books WHERE id > 100 ORDER BY id desc LIMIT 20

您也可以取消特定 where 子句的範圍。例如,這將從 where 子句中移除 id 條件。

Book.where(id: 10, out_of_print: false).unscope(where: :id)
# SELECT books.* FROM books WHERE out_of_print = 0

已使用 unscope 的關係將會影響其合併的任何關係。

Book.order("id desc").merge(Book.unscope(:order))
# SELECT books.* FROM books

8.2 only

您也可以使用 only 方法覆寫條件。例如:

Book.where("id > 10").limit(20).order("id desc").only(:order, :where)

將執行的 SQL:

SELECT * FROM books WHERE id > 10 ORDER BY id DESC

-- Original query without `only`
SELECT * FROM books WHERE id > 10 ORDER BY id DESC LIMIT 20

8.3 reselect

reselect 方法會覆寫現有的選取語句。例如:

Book.select(:title, :isbn).reselect(:created_at)

將執行的 SQL:

SELECT books.created_at FROM books

將其與未使用 reselect 子句的情況進行比較:

Book.select(:title, :isbn).select(:created_at)

執行的 SQL 將會是:

SELECT books.title, books.isbn, books.created_at FROM books

8.4 reorder

reorder 方法會覆寫預設範圍排序。例如,如果類別定義包含:

class Author < ApplicationRecord
  has_many :books, -> { order(year_published: :desc) }
end

並且您執行了:

Author.find(10).books

將執行的 SQL:

SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published DESC

您可以使用 reorder 子句來指定不同的書籍排序方式。

Author.find(10).books.reorder("year_published ASC")

將執行的 SQL:

SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published ASC

8.5 reverse_order

如果指定了排序子句,則 reverse_order 方法會反轉排序子句。

Book.where("author_id > 10").order(:year_published).reverse_order

將執行的 SQL:

SELECT * FROM books WHERE author_id > 10 ORDER BY year_published DESC

如果查詢中未指定排序子句,則 reverse_order 會依反向順序依主鍵排序。

Book.where("author_id > 10").reverse_order

將執行的 SQL:

SELECT * FROM books WHERE author_id > 10 ORDER BY books.id DESC

reverse_order 方法不接受任何引數。

8.6 rewhere

rewhere 方法會覆寫現有的具名 where 條件。例如:

Book.where(out_of_print: true).rewhere(out_of_print: false)

將執行的 SQL:

SELECT * FROM books WHERE out_of_print = 0

如果未使用 rewhere 子句,則 where 子句會以 AND 方式組合在一起。

Book.where(out_of_print: true).where(out_of_print: false)

執行的 SQL 將會是:

SELECT * FROM books WHERE out_of_print = 1 AND out_of_print = 0

8.7 regroup

regroup 方法會覆寫現有的具名 group 條件。例如:

Book.group(:author).regroup(:id)

將執行的 SQL:

SELECT * FROM books GROUP BY id

如果未使用 regroup 子句,則 group 子句會組合在一起。

Book.group(:author).group(:id)

執行的 SQL 將會是:

SELECT * FROM books GROUP BY author, id

9 空值關係

none 方法會傳回沒有記錄的可鏈接關係。附加到傳回關係的任何後續條件將會繼續產生空關係。這在需要可鏈接回應方法或可能傳回零結果的範圍的情境中很有用。

Book.none # returns an empty Relation and fires no queries.
# The highlighted_reviews method below is expected to always return a Relation.
Book.first.highlighted_reviews.average(:rating)
# => Returns average rating of a book

class Book
  # Returns reviews if there are at least 5,
  # else consider this as non-reviewed book
  def highlighted_reviews
    if reviews.count > 5
      reviews
    else
      Review.none # Does not meet minimum threshold yet
    end
  end
end

10 唯讀物件

Active Record 在關聯(relation)上提供 readonly 方法,明確禁止修改任何回傳的物件。任何嘗試修改唯讀記錄的行為都會失敗,並引發 ActiveRecord::ReadOnlyRecord 例外。

customer = Customer.readonly.first
customer.visits += 1
customer.save # Raises an ActiveRecord::ReadOnlyRecord

由於 customer 被明確設定為唯讀物件,上述程式碼在呼叫 customer.save 並更新 visits 的值時,會引發 ActiveRecord::ReadOnlyRecord 例外。

11 鎖定記錄以進行更新

鎖定有助於防止在更新資料庫中的記錄時發生競爭條件,並確保更新的原子性。

Active Record 提供兩種鎖定機制

  • 樂觀鎖定
  • 悲觀鎖定

11.1 樂觀鎖定

樂觀鎖定允許多個使用者存取相同的記錄進行編輯,並假設資料衝突的可能性很小。它的運作方式是檢查自記錄開啟以來,是否有其他程序對記錄進行了變更。如果發生這種情況,則會拋出 ActiveRecord::StaleObjectError 例外,並忽略更新。

樂觀鎖定欄位

為了使用樂觀鎖定,資料表需要有一個名為 lock_version 的整數型別欄位。每次更新記錄時,Active Record 都會遞增 lock_version 欄位。如果在 lock_version 欄位中的值低於資料庫中 lock_version 欄位的當前值的情況下提出更新請求,則更新請求將會失敗,並產生 ActiveRecord::StaleObjectError

例如:

c1 = Customer.find(1)
c2 = Customer.find(1)

c1.first_name = "Sandra"
c1.save

c2.first_name = "Michael"
c2.save # Raises an ActiveRecord::StaleObjectError

您有責任處理衝突,方法是捕捉例外並回滾、合併或以其他方式應用解決衝突所需的業務邏輯。

可以透過設定 ActiveRecord::Base.lock_optimistically = false 來關閉此行為。

要覆寫 lock_version 欄位的名稱,ActiveRecord::Base 提供了一個名為 locking_column 的類別屬性

class Customer < ApplicationRecord
  self.locking_column = :lock_customer_column
end

11.2 悲觀鎖定

悲觀鎖定使用底層資料庫提供的鎖定機制。在建立關聯時使用 lock 會取得選取列的獨佔鎖定。使用 lock 的關聯通常會封裝在交易中,以防止死鎖情況。

例如:

Book.transaction do
  book = Book.lock.first
  book.title = "Algorithms, second edition"
  book.save!
end

上述的會話(session)會為 MySQL 後端產生以下 SQL

SQL (0.2ms)   BEGIN
Book Load (0.3ms)   SELECT * FROM books LIMIT 1 FOR UPDATE
Book Update (0.4ms)   UPDATE books SET updated_at = '2009-02-07 18:05:56', title = 'Algorithms, second edition' WHERE id = 1
SQL (0.8ms)   COMMIT

您也可以將原始 SQL 傳遞給 lock 方法,以允許不同類型的鎖定。例如,MySQL 有一個名為 LOCK IN SHARE MODE 的運算式,您可以在鎖定記錄的同時仍然允許其他查詢讀取它。若要指定此運算式,只需將其作為鎖定選項傳入即可

Book.transaction do
  book = Book.lock("LOCK IN SHARE MODE").find(1)
  book.increment!(:views)
end

請注意,您的資料庫必須支援您傳遞給 lock 方法的原始 SQL。

如果您已經有模型的實例,您可以使用以下程式碼來啟動交易並同時取得鎖定

book = Book.first
book.with_lock do
  # This block is called within a transaction,
  # book is already locked.
  book.increment!(:views)
end

12 連接資料表

Active Record 提供兩種查找方法,用於在產生的 SQL 中指定 JOIN 子句:joinsleft_outer_joinsjoins 應用於 INNER JOIN 或自訂查詢,而 left_outer_joins 應用於使用 LEFT OUTER JOIN 的查詢。

12.1 joins

有多種使用 joins 方法的方式。

12.1.1 使用字串 SQL 片段

您可以直接將指定 JOIN 子句的原始 SQL 提供給 joins

Author.joins("INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE")

這將產生以下 SQL

SELECT authors.* FROM authors INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE

12.1.2 使用具名關聯的陣列/雜湊

Active Record 讓您可以使用模型上定義的關聯名稱,作為在使用 joins 方法時指定這些關聯的 JOIN 子句的捷徑。

以下所有範例都會使用 INNER JOIN 產生預期的連接查詢

12.1.2.1 連接單一關聯
Book.joins(:reviews)

這會產生

SELECT books.* FROM books
  INNER JOIN reviews ON reviews.book_id = books.id

或者,用英文來說:「回傳所有有評論的書籍的 Book 物件」。請注意,如果一本書有多個評論,您會看到重複的書籍。如果您想要不重複的書籍,可以使用 Book.joins(:reviews).distinct

12.1.3 連接多個關聯

Book.joins(:author, :reviews)

這會產生

SELECT books.* FROM books
  INNER JOIN authors ON authors.id = books.author_id
  INNER JOIN reviews ON reviews.book_id = books.id

或者,用英文來說:「回傳所有有作者且至少有一個評論的書籍」。再次提醒,有多個評論的書籍會多次出現。

12.1.3.1 連接巢狀關聯(單層)
Book.joins(reviews: :customer)

這會產生

SELECT books.* FROM books
  INNER JOIN reviews ON reviews.book_id = books.id
  INNER JOIN customers ON customers.id = reviews.customer_id

或者,用英文來說:「回傳所有有客戶評論的書籍。」

12.1.3.2 連接巢狀關聯(多層)
Author.joins(books: [{ reviews: { customer: :orders } }, :supplier])

這會產生

SELECT authors.* FROM authors
  INNER JOIN books ON books.author_id = authors.id
  INNER JOIN reviews ON reviews.book_id = books.id
  INNER JOIN customers ON customers.id = reviews.customer_id
  INNER JOIN orders ON orders.customer_id = customers.id
INNER JOIN suppliers ON suppliers.id = books.supplier_id

或者,用英文來說:「回傳所有有附帶評論的書籍的作者,以及訂購這些書籍的客戶,以及這些書籍的供應商。」

12.1.4 指定連接資料表的條件

您可以使用一般陣列字串條件,在連接資料表上指定條件。雜湊條件提供了一種特殊的語法,用於指定連接資料表的條件

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where("orders.created_at" => time_range).distinct

這會使用 BETWEEN SQL 運算式來比較 created_at,找出所有昨天有訂單的客戶。

一種替代且更簡潔的語法是巢狀雜湊條件

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where(orders: { created_at: time_range }).distinct

若要取得更進階的條件或重複使用現有的具名範圍,可以使用 merge。首先,讓我們在 Order 模型中新增一個新的具名範圍

class Order < ApplicationRecord
  belongs_to :customer

  scope :created_in_time_range, ->(time_range) {
    where(created_at: time_range)
  }
end

現在,我們可以利用 merge 合併 created_in_time_range 範圍

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).merge(Order.created_in_time_range(time_range)).distinct

這會使用 BETWEEN SQL 運算式,再次找出所有昨天有訂單的客戶。

12.2 left_outer_joins

如果您想要選取一組記錄,無論它們是否有相關記錄,您可以使用 left_outer_joins 方法。

Customer.left_outer_joins(:reviews).distinct.select("customers.*, COUNT(reviews.*) AS reviews_count").group("customers.id")

這會產生

SELECT DISTINCT customers.*, COUNT(reviews.*) AS reviews_count FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id GROUP BY customers.id

意思是:「回傳所有客戶及其評論計數,無論他們是否有任何評論。」

12.3 where.associatedwhere.missing

associatedmissing 查詢方法可讓您根據關聯是否存在來選取一組記錄。

要使用 where.associated

Customer.where.associated(:reviews)

產生

SELECT customers.* FROM customers
INNER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NOT NULL

意思是:「回傳所有至少撰寫過一篇評論的客戶」。

要使用 where.missing

Customer.where.missing(:reviews)

產生

SELECT customers.* FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NULL

意思是:「回傳所有尚未撰寫任何評論的客戶」。

13 急切載入關聯

急切載入是一種機制,用於使用盡可能少的查詢來載入 Model.find 回傳的物件的相關記錄。

13.1 N + 1 查詢問題

考量以下程式碼,它會找出 10 本書並列印其作者的姓氏

books = Book.limit(10)

books.each do |book|
  puts book.author.last_name
end

乍看之下,這段程式碼似乎沒什麼問題。但問題在於執行的查詢總數。上述程式碼總共會執行 1 個查詢(用於找出 10 本書) + 10 個查詢(每本書一個,用於載入作者) = 11 個查詢。

13.1.1 N + 1 查詢問題的解決方案

Active Record 讓您可以預先指定要載入的所有關聯。

這些方法包括

13.2 includes

使用 includes 時,Active Record 可確保使用最少的查詢次數來載入所有指定的關聯。

使用 includes 方法重新檢視上述案例,我們可以將 Book.limit(10) 重寫為急切載入作者

books = Book.includes(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

上述程式碼只會執行 2 個查詢,而不是原始案例中的 11 個查詢

SELECT books.* FROM books LIMIT 10
SELECT authors.* FROM authors
  WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)

13.2.1 急切載入多個關聯

Active Record 讓您可以使用陣列、雜湊或含有 includes 方法的陣列/雜湊的巢狀雜湊,透過單一 Model.find 呼叫來急切載入任意數量的關聯。

13.2.1.1 多個關聯的陣列
Customer.includes(:orders, :reviews)

這會載入所有客戶以及每個客戶的相關訂單和評論。

13.2.1.2 巢狀關聯雜湊
Customer.includes(orders: { books: [:supplier, :author] }).find(1)

這會找出 ID 為 1 的客戶,並急切載入其所有相關訂單、所有訂單的書籍,以及每本書的作者和供應商。

13.2.2 指定急切載入關聯的條件

即使 Active Record 讓您可以像 joins 一樣指定急切載入關聯的條件,建議的方式仍然是改用joins

但是,如果您必須這麼做,您可以像平常一樣使用 where

Author.includes(:books).where(books: { out_of_print: true })

這會產生包含 LEFT OUTER JOIN 的查詢,而 joins 方法會產生使用 INNER JOIN 函數的查詢。

  SELECT authors.id AS t0_r0, ... books.updated_at AS t1_r5 FROM authors LEFT OUTER JOIN books ON books.author_id = authors.id WHERE (books.out_of_print = 1)

如果沒有 where 條件,這會產生一般的兩組查詢。

像這樣使用 where 只會在您傳遞雜湊時有效。對於 SQL 片段,您需要使用 references 來強制連接資料表

Author.includes(:books).where("books.out_of_print = true").references(:books)

在這種 includes 查詢的情況下,如果沒有任何作者的書籍,所有作者仍然會被載入。透過使用 joins (一種 INNER JOIN),連接條件必須符合,否則不會回傳任何記錄。

如果將關聯作為連接的一部分急切載入,則自訂選取子句中的任何欄位都不會出現在載入的模型上。這是因為它們應該出現在父記錄還是子記錄上存在歧義。

13.3 preload

使用 preload 時,Active Record 會使用每個關聯一個查詢來載入每個指定的關聯。

重新檢視 N + 1 查詢問題,我們可以將 Book.limit(10) 重寫為預先載入作者

books = Book.preload(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

上述程式碼只會執行 2 個查詢,而不是原始案例中的 11 個查詢

SELECT books.* FROM books LIMIT 10
SELECT authors.* FROM authors
  WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)

preload 方法使用陣列、雜湊或陣列/雜湊的巢狀雜湊,與 includes 方法的方式相同,以便透過單一 Model.find 呼叫來載入任意數量的關聯。但是,與 includes 方法不同,無法為預先載入的關聯指定條件。

13.4 eager_load

使用 eager_load 時,Active Record 會使用 LEFT OUTER JOIN 來載入所有指定的關聯。

使用 eager_load 方法重新檢視發生 N + 1 情況的案例,我們可以將 Book.limit(10) 重寫為載入作者

books = Book.eager_load(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

上述程式碼只會執行 1 個查詢,而不是原始案例中的 11 個查詢

SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, ... FROM "books"
  LEFT OUTER JOIN "authors" ON "authors"."id" = "books"."author_id"
  LIMIT 10

eager_load 方法使用陣列、雜湊或陣列/雜湊的巢狀雜湊,與 includes 方法的方式相同,以便透過單一 Model.find 呼叫來載入任意數量的關聯。此外,與 includes 方法類似,您可以為急切載入的關聯指定條件。

13.5 strict_loading

急切載入可以防止 N + 1 查詢,但您可能仍然會延遲載入某些關聯。為了確保不會延遲載入任何關聯,您可以啟用 strict_loading

透過在關聯上啟用嚴格載入模式,如果記錄嘗試延遲載入任何關聯,則會引發 ActiveRecord::StrictLoadingViolationError

user = User.strict_loading.first
user.address.city # raises an ActiveRecord::StrictLoadingViolationError
user.comments.to_a # raises an ActiveRecord::StrictLoadingViolationError

若要為所有關聯啟用嚴格載入,請將 config.active_record.strict_loading_by_default 旗標變更為 true

若要將違規行為發送到記錄器,請將 config.active_record.action_on_strict_loading_violation 變更為 :log

13.6 strict_loading!

我們也可以透過呼叫 strict_loading! 來在記錄本身啟用嚴格載入。

user = User.first
user.strict_loading!
user.address.city # raises an ActiveRecord::StrictLoadingViolationError
user.comments.to_a # raises an ActiveRecord::StrictLoadingViolationError

strict_loading! 也接受 :mode 參數。將其設定為 :n_plus_one_only 只會在延遲載入會導致 N + 1 查詢的關聯時引發錯誤。

user.strict_loading!(mode: :n_plus_one_only)
user.address.city # => "Tatooine"
user.comments.to_a # => [#<Comment:0x00...]
user.comments.first.likes.to_a # raises an ActiveRecord::StrictLoadingViolationError

13.7 關聯上的 strict_loading 選項

我們也可以透過提供 strict_loading 選項來為單一關聯啟用嚴格載入。

class Author < ApplicationRecord
  has_many :books, strict_loading: true
end

14 作用域 (Scopes)

作用域允許您指定常用查詢,這些查詢可以作為關聯物件或模型上的方法呼叫來引用。使用這些作用域,您可以使用先前涵蓋的所有方法,例如 wherejoinsincludes。所有作用域主體都應該返回一個 ActiveRecord::Relationnil,以便可以在其上呼叫更多方法(例如其他作用域)。

若要定義一個簡單的作用域,我們在類別內部使用 scope 方法,並傳遞我們希望在呼叫此作用域時執行的查詢。

class Book < ApplicationRecord
  scope :out_of_print, -> { where(out_of_print: true) }
end

若要呼叫這個 out_of_print 作用域,我們可以在類別上呼叫它

irb> Book.out_of_print
=> #<ActiveRecord::Relation> # all out of print books

或在由 Book 物件組成的關聯上呼叫它

irb> author = Author.first
irb> author.books.out_of_print
=> #<ActiveRecord::Relation> # all out of print books by `author`

作用域也可以在作用域內鏈接

class Book < ApplicationRecord
  scope :out_of_print, -> { where(out_of_print: true) }
  scope :out_of_print_and_expensive, -> { out_of_print.where("price > 500") }
end

14.1 傳遞參數

您的作用域可以接受參數

class Book < ApplicationRecord
  scope :costs_more_than, ->(amount) { where("price > ?", amount) }
end

像呼叫類別方法一樣呼叫作用域

irb> Book.costs_more_than(100.10)

但是,這只是重複類別方法會提供的功能。

class Book < ApplicationRecord
  def self.costs_more_than(amount)
    where("price > ?", amount)
  end
end

這些方法仍然可以在關聯物件上存取

irb> author.books.costs_more_than(100.10)

14.2 使用條件式

您的作用域可以利用條件式

class Order < ApplicationRecord
  scope :created_before, ->(time) { where(created_at: ...time) if time.present? }
end

與其他範例一樣,這將與類別方法的行為類似。

class Order < ApplicationRecord
  def self.created_before(time)
    where(created_at: ...time) if time.present?
  end
end

但是,有一個重要的注意事項:即使條件式評估為 false,作用域也總是返回一個 ActiveRecord::Relation 物件,而類別方法會返回 nil。如果任何條件式返回 false,這可能會在將類別方法與條件式鏈接時導致 NoMethodError

14.3 應用預設作用域

如果我們希望將作用域應用於模型的所有查詢,我們可以在模型本身中使用 default_scope 方法。

class Book < ApplicationRecord
  default_scope { where(out_of_print: false) }
end

當在此模型上執行查詢時,SQL 查詢現在看起來會像這樣

SELECT * FROM books WHERE (out_of_print = false)

如果您需要對預設作用域執行更複雜的操作,您可以選擇將其定義為類別方法

class Book < ApplicationRecord
  def self.default_scope
    # Should return an ActiveRecord::Relation.
  end
end

當作用域參數以 Hash 形式給出時,在建立/建構記錄時也會套用 default_scope。它不會在更新記錄時套用。例如:

class Book < ApplicationRecord
  default_scope { where(out_of_print: false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: false>
irb> Book.unscoped.new
=> #<Book id: nil, out_of_print: nil>

請注意,當以 Array 格式給出時,default_scope 查詢參數無法轉換為 Hash 以進行預設屬性指派。例如:

class Book < ApplicationRecord
  default_scope { where("out_of_print = ?", false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: nil>

14.4 合併作用域

就像 where 子句一樣,作用域使用 AND 條件合併。

class Book < ApplicationRecord
  scope :in_print, -> { where(out_of_print: false) }
  scope :out_of_print, -> { where(out_of_print: true) }

  scope :recent, -> { where(year_published: 50.years.ago.year..) }
  scope :old, -> { where(year_published: ...50.years.ago.year) }
end
irb> Book.out_of_print.old
SELECT books.* FROM books WHERE books.out_of_print = 'true' AND books.year_published < 1969

我們可以混合和匹配 scopewhere 條件,最終的 SQL 將所有條件與 AND 連接。

irb> Book.in_print.where(price: ...100)
SELECT books.* FROM books WHERE books.out_of_print = 'false' AND books.price < 100

如果我們確實希望最後一個 where 子句獲勝,則可以使用 merge

irb> Book.in_print.merge(Book.out_of_print)
SELECT books.* FROM books WHERE books.out_of_print = true

一個重要的注意事項是,default_scope 將在 scopewhere 條件中前置。

class Book < ApplicationRecord
  default_scope { where(year_published: 50.years.ago.year..) }

  scope :in_print, -> { where(out_of_print: false) }
  scope :out_of_print, -> { where(out_of_print: true) }
end
irb> Book.all
SELECT books.* FROM books WHERE (year_published >= 1969)

irb> Book.in_print
SELECT books.* FROM books WHERE (year_published >= 1969) AND books.out_of_print = false

irb> Book.where('price > 50')
SELECT books.* FROM books WHERE (year_published >= 1969) AND (price > 50)

如您在上面看到的,default_scope 正在 scopewhere 條件中合併。

14.5 移除所有作用域

如果我們因任何原因要移除作用域,可以使用 unscoped 方法。如果模型中指定了 default_scope 且不應將其套用於此特定查詢,則此方法特別有用。

Book.unscoped.load

此方法會移除所有作用域,並對資料表執行一般查詢。

irb> Book.unscoped.all
SELECT books.* FROM books

irb> Book.where(out_of_print: true).unscoped.all
SELECT books.* FROM books

unscoped 也可以接受區塊

irb> Book.unscoped { Book.out_of_print }
SELECT books.* FROM books WHERE books.out_of_print = true

15 動態尋找器

對於您在資料表中定義的每個欄位(也稱為屬性),Active Record 都會提供一個尋找器方法。例如,如果您在 Customer 模型上有名為 first_name 的欄位,您將免費從 Active Record 獲得實例方法 find_by_first_name。如果您在 Customer 模型上也有 locked 欄位,您也會獲得 find_by_locked 方法。

您可以在動態尋找器的末尾指定驚嘆號 (!),讓它們在沒有傳回任何記錄時引發 ActiveRecord::RecordNotFound 錯誤,例如 Customer.find_by_first_name!("Ryan")

如果您想同時透過 first_nameorders_count 尋找,您可以簡單地在欄位之間輸入「and」來將這些尋找器鏈接在一起。例如,Customer.find_by_first_name_and_orders_count("Ryan", 5)

16 列舉

列舉讓您可以為屬性定義一個數值陣列,並依名稱引用它們。儲存在資料庫中的實際值是一個已對應到其中一個數值的整數。

宣告列舉將會

  • 建立作用域,可用於尋找所有具有或不具有其中一個列舉值的物件
  • 建立實例方法,可用於判斷物件是否具有列舉的特定值
  • 建立實例方法,可用於變更物件的列舉值

適用於列舉的所有可能值。

例如,給定此 enum 宣告

class Order < ApplicationRecord
  enum :status, [:shipped, :being_packaged, :complete, :cancelled]
end

這些 作用域 會自動建立,可用於尋找所有 status 具有或不具有特定值的物件

irb> Order.shipped
=> #<ActiveRecord::Relation> # all orders with status == :shipped
irb> Order.not_shipped
=> #<ActiveRecord::Relation> # all orders with status != :shipped

這些實例方法會自動建立,並查詢模型是否具有該 status 列舉值

irb> order = Order.shipped.first
irb> order.shipped?
=> true
irb> order.complete?
=> false

這些實例方法會自動建立,並首先將 status 的值更新為具名值,然後查詢狀態是否已成功設定為該值

irb> order = Order.first
irb> order.shipped!
UPDATE "orders" SET "status" = ?, "updated_at" = ? WHERE "orders"."id" = ?  [["status", 0], ["updated_at", "2019-01-24 07:13:08.524320"], ["id", 1]]
=> true

有關列舉的完整文件,請參閱這裡

17 了解方法鏈接

Active Record 模式實作了方法鏈接,這讓您可以簡單明瞭的方式一起使用多個 Active Record 方法。

當先前呼叫的方法傳回 ActiveRecord::Relation 時,您可以在語句中鏈接方法,例如 allwherejoins。傳回單一物件的方法(請參閱 擷取單一物件章節)必須位於語句的末尾。

以下有一些範例。本指南不會涵蓋所有可能性,僅提供一些範例。當呼叫 Active Record 方法時,不會立即產生查詢並傳送到資料庫。只有在實際需要資料時才會傳送查詢。因此,以下每個範例都會產生單一查詢。

17.1 從多個資料表擷取篩選的資料

Customer
  .select("customers.id, customers.last_name, reviews.body")
  .joins(:reviews)
  .where("reviews.created_at > ?", 1.week.ago)

結果應該如下所示

SELECT customers.id, customers.last_name, reviews.body
FROM customers
INNER JOIN reviews
  ON reviews.customer_id = customers.id
WHERE (reviews.created_at > '2019-01-08')

17.2 從多個資料表擷取特定資料

Book
  .select("books.id, books.title, authors.first_name")
  .joins(:author)
  .find_by(title: "Abstraction and Specification in Program Development")

以上應產生

SELECT books.id, books.title, authors.first_name
FROM books
INNER JOIN authors
  ON authors.id = books.author_id
WHERE books.title = $1 [["title", "Abstraction and Specification in Program Development"]]
LIMIT 1

請注意,如果查詢符合多個記錄,find_by 將僅擷取第一個記錄,並忽略其他記錄(請參閱上面的 LIMIT 1 語句)。

18 尋找或建立新物件

您通常需要尋找記錄,如果記錄不存在,則建立記錄。您可以使用 find_or_create_byfind_or_create_by! 方法來執行此操作。

18.1 find_or_create_by

find_or_create_by 方法會檢查是否存在具有指定屬性的記錄。如果不存在,則會呼叫 create。讓我們看一個範例。

假設您要尋找名為「Andy」的客戶,如果沒有,則建立一個客戶。您可以透過執行下列操作來執行此操作

irb> Customer.find_or_create_by(first_name: 'Andy')
=> #<Customer id: 5, first_name: "Andy", last_name: nil, title: nil, visits: 0, orders_count: nil, lock_version: 0, created_at: "2019-01-17 07:06:45", updated_at: "2019-01-17 07:06:45">

此方法產生的 SQL 如下所示

SELECT * FROM customers WHERE (customers.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO customers (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT

find_or_create_by 會傳回已存在的記錄或新記錄。在我們的案例中,我們還沒有名為 Andy 的客戶,因此會建立並傳回該記錄。

新記錄可能不會儲存到資料庫中;這取決於驗證是否通過(就像 create 一樣)。

假設我們要在建立新記錄時將 'locked' 屬性設定為 false,但我們不想將其包含在查詢中。因此,我們想要尋找名為「Andy」的客戶,或者如果該客戶不存在,則建立一個名為「Andy」且未鎖定的客戶。

我們可以透過兩種方式達成此目的。第一種是使用 create_with

Customer.create_with(locked: false).find_or_create_by(first_name: "Andy")

第二種方式是使用區塊

Customer.find_or_create_by(first_name: "Andy") do |c|
  c.locked = false
end

只有在建立客戶時才會執行區塊。第二次執行此程式碼時,將會忽略區塊。

18.2 find_or_create_by!

您也可以使用 find_or_create_by! 在新記錄無效時引發例外。本指南未涵蓋驗證,但讓我們暫時假設您新增了

validates :orders_count, presence: true

到您的 Customer 模型。如果您嘗試建立新的 Customer 但未傳遞 orders_count,則記錄將無效,並且將會引發例外

irb> Customer.find_or_create_by!(first_name: 'Andy')
ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank

18.3 find_or_initialize_by

find_or_initialize_by 方法的運作方式與 find_or_create_by 相同,但它會呼叫 new 而不是 create。這表示將會在記憶體中建立新的模型實例,但不會儲存到資料庫。繼續使用 find_or_create_by 範例,我們現在想要名為「Nina」的客戶

irb> nina = Customer.find_or_initialize_by(first_name: 'Nina')
=> #<Customer id: nil, first_name: "Nina", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">

irb> nina.persisted?
=> false

irb> nina.new_record?
=> true

由於物件尚未儲存在資料庫中,因此產生的 SQL 如下所示

SELECT * FROM customers WHERE (customers.first_name = 'Nina') LIMIT 1

當您想要將其儲存到資料庫時,只需呼叫 save

irb> nina.save
=> true

19 依 SQL 尋找

如果您想使用自己的 SQL 來尋找資料表中的記錄,可以使用 find_by_sql。即使基礎查詢僅傳回單一記錄,find_by_sql 方法也會傳回物件陣列。例如,您可以執行此查詢

irb> Customer.find_by_sql("SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id ORDER BY customers.created_at desc")
=> [#<Customer id: 1, first_name: "Lucas" ...>, #<Customer id: 2, first_name: "Jan" ...>, ...]

find_by_sql 提供了一種簡單的方法來對資料庫進行自訂呼叫並擷取已實例化的物件。

19.1 select_all

find_by_sql 有一個密切相關的方法,稱為 lease_connection.select_allselect_all 將使用自訂 SQL 從資料庫擷取物件,就像 find_by_sql 一樣,但不會實例化它們。此方法將傳回 ActiveRecord::Result 類別的實例,並且對此物件呼叫 to_a 將會傳回一個雜湊陣列,其中每個雜湊都表示一個記錄。

irb> Customer.lease_connection.select_all("SELECT first_name, created_at FROM customers WHERE id = '1'").to_a
=> [{"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}]

19.2 pluck

pluck 可用於從目前關聯中的具名欄位選取值。它接受欄位名稱列表作為參數,並傳回指定欄位的值陣列,其中包含相應的資料類型。

irb> Book.where(out_of_print: true).pluck(:id)
SELECT id FROM books WHERE out_of_print = true
=> [1, 2, 3]

irb> Order.distinct.pluck(:status)
SELECT DISTINCT status FROM orders
=> ["shipped", "being_packed", "cancelled"]

irb> Customer.pluck(:id, :first_name)
SELECT customers.id, customers.first_name FROM customers
=> [[1, "David"], [2, "Fran"], [3, "Jose"]]

pluck 可以取代類似以下的程式碼

Customer.select(:id).map { |c| c.id }
# or
Customer.select(:id).map(&:id)
# or
Customer.select(:id, :first_name).map { |c| [c.id, c.first_name] }

使用

Customer.pluck(:id)
# or
Customer.pluck(:id, :first_name)

select 不同,pluck 直接將資料庫結果轉換為 Ruby 的 Array,而不會建構 ActiveRecord 物件。這表示對於大型或頻繁執行的查詢來說,效能可能更好。但是,任何模型方法的覆寫將無法使用。例如

class Customer < ApplicationRecord
  def name
    "I am #{first_name}"
  end
end
irb> Customer.select(:first_name).map &:name
=> ["I am David", "I am Jeremy", "I am Jose"]

irb> Customer.pluck(:first_name)
=> ["David", "Jeremy", "Jose"]

你不僅限於查詢單一表格的欄位,你也可以查詢多個表格。

irb> Order.joins(:customer, :books).pluck("orders.created_at, customers.email, books.title")

此外,與 select 和其他 Relation 範圍不同,pluck 會觸發立即查詢,因此無法與任何進一步的範圍鏈接,儘管它可以與先前已建構的範圍一起使用。

irb> Customer.pluck(:first_name).limit(1)
NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>

irb> Customer.limit(1).pluck(:first_name)
=> ["David"]

你也應該知道,如果關係物件包含 include 值,即使查詢不需要預先載入,使用 pluck 也會觸發預先載入。例如

irb> assoc = Customer.includes(:reviews)
irb> assoc.pluck(:id)
SELECT "customers"."id" FROM "customers" LEFT OUTER JOIN "reviews" ON "reviews"."id" = "customers"."review_id"

避免這種情況的一種方法是使用 unscope 取消 include。

irb> assoc.unscope(:includes).pluck(:id)

19.3 pick

pick 可以用來從目前關係中選取指定欄位的數值。它接受一個欄位名稱列表作為參數,並返回指定欄位值的第一行,並帶有對應的資料型別。pickrelation.limit(1).pluck(*column_names).first 的簡寫,當你已經有一個限制為一列的關係時,這特別有用。

pick 可以取代像這樣的程式碼

Customer.where(id: 1).pluck(:id).first

使用

Customer.where(id: 1).pick(:id)

19.4 ids

ids 可以用來使用表格的主鍵擷取關係的所有 ID。

irb> Customer.ids
SELECT id FROM customers
class Customer < ApplicationRecord
  self.primary_key = "customer_id"
end
irb> Customer.ids
SELECT customer_id FROM customers

20 物件的存在性

如果你只想檢查物件是否存在,可以使用名為 exists? 的方法。此方法將使用與 find 相同的查詢來查詢資料庫,但它會返回 truefalse,而不是返回物件或物件集合。

Customer.exists?(1)

exists? 方法也接受多個值,但要注意的是,如果其中任何一個記錄存在,它將返回 true

Customer.exists?(id: [1, 2, 3])
# or
Customer.exists?(first_name: ["Jane", "Sergei"])

甚至可以在模型或關係上不帶任何參數地使用 exists?

Customer.where(first_name: "Ryan").exists?

如果至少有一個 first_name 為 'Ryan' 的客戶,則以上程式碼會返回 true,否則返回 false

Customer.exists?

如果 customers 表格為空,則以上程式碼會返回 false,否則返回 true

你也可以使用 any?many? 來檢查模型或關係是否存在。many? 將使用 SQL count 來判斷項目是否存在。

# via a model
Order.any?
# SELECT 1 FROM orders LIMIT 1
Order.many?
# SELECT COUNT(*) FROM (SELECT 1 FROM orders LIMIT 2)

# via a named scope
Order.shipped.any?
# SELECT 1 FROM orders WHERE orders.status = 0 LIMIT 1
Order.shipped.many?
# SELECT COUNT(*) FROM (SELECT 1 FROM orders WHERE orders.status = 0 LIMIT 2)

# via a relation
Book.where(out_of_print: true).any?
Book.where(out_of_print: true).many?

# via an association
Customer.first.orders.any?
Customer.first.orders.many?

21 計算

此章節使用 count 作為範例方法,但所描述的選項適用於所有子章節。

所有計算方法都直接在模型上運作

irb> Customer.count
SELECT COUNT(*) FROM customers

或在關係上運作

irb> Customer.where(first_name: 'Ryan').count
SELECT COUNT(*) FROM customers WHERE (first_name = 'Ryan')

你也可以在關係上使用各種尋找器方法來執行複雜的計算

irb> Customer.includes("orders").where(first_name: 'Ryan', orders: { status: 'shipped' }).count

這將執行

SELECT COUNT(DISTINCT customers.id) FROM customers
  LEFT OUTER JOIN orders ON orders.customer_id = customers.id
  WHERE (customers.first_name = 'Ryan' AND orders.status = 0)

假設 Order 有 enum status: [ :shipped, :being_packed, :cancelled ]

21.1 count

如果你想查看模型表格中有多少記錄,你可以呼叫 Customer.count,這將返回數字。如果你想更精確地找到資料庫中所有具有標題的客戶,你可以使用 Customer.count(:title)

如需選項,請參閱父章節:計算

21.2 average

如果你想查看其中一個表格中某個數字的平均值,你可以呼叫與該表格相關的類別的 average 方法。此方法呼叫看起來會像這樣

Order.average("subtotal")

這將返回一個數字(可能是浮點數,例如 3.14159265),表示該欄位中的平均值。

如需選項,請參閱父章節:計算

21.3 minimum

如果你想在表格中找到某個欄位的最小值,你可以呼叫與該表格相關的類別的 minimum 方法。此方法呼叫看起來會像這樣

Order.minimum("subtotal")

如需選項,請參閱父章節:計算

21.4 maximum

如果你想在表格中找到某個欄位的最大值,你可以呼叫與該表格相關的類別的 maximum 方法。此方法呼叫看起來會像這樣

Order.maximum("subtotal")

如需選項,請參閱父章節:計算

21.5 sum

如果你想找到表格中所有記錄的某個欄位的總和,你可以呼叫與該表格相關的類別的 sum 方法。此方法呼叫看起來會像這樣

Order.sum("subtotal")

如需選項,請參閱父章節:計算

22 執行 EXPLAIN

你可以在關係上執行 explain。EXPLAIN 的輸出會因每個資料庫而異。

例如,執行

Customer.where(id: 1).joins(:orders).explain

對於 MySQL 和 MariaDB,可能會產生此結果

EXPLAIN SELECT `customers`.* FROM `customers` INNER JOIN `orders` ON `orders`.`customer_id` = `customers`.`id` WHERE `customers`.`id` = 1
+----+-------------+------------+-------+---------------+
| id | select_type | table      | type  | possible_keys |
+----+-------------+------------+-------+---------------+
|  1 | SIMPLE      | customers  | const | PRIMARY       |
|  1 | SIMPLE      | orders     | ALL   | NULL          |
+----+-------------+------------+-------+---------------+
+---------+---------+-------+------+-------------+
| key     | key_len | ref   | rows | Extra       |
+---------+---------+-------+------+-------------+
| PRIMARY | 4       | const |    1 |             |
| NULL    | NULL    | NULL  |    1 | Using where |
+---------+---------+-------+------+-------------+

2 rows in set (0.00 sec)

Active Record 會執行一個仿效相應資料庫 shell 的美觀列印。因此,使用 PostgreSQL 配接器執行的相同查詢會產生

EXPLAIN SELECT "customers".* FROM "customers" INNER JOIN "orders" ON "orders"."customer_id" = "customers"."id" WHERE "customers"."id" = $1 [["id", 1]]
                                  QUERY PLAN
------------------------------------------------------------------------------
 Nested Loop  (cost=4.33..20.85 rows=4 width=164)
    ->  Index Scan using customers_pkey on customers  (cost=0.15..8.17 rows=1 width=164)
          Index Cond: (id = '1'::bigint)
    ->  Bitmap Heap Scan on orders  (cost=4.18..12.64 rows=4 width=8)
          Recheck Cond: (customer_id = '1'::bigint)
          ->  Bitmap Index Scan on index_orders_on_customer_id  (cost=0.00..4.18 rows=4 width=0)
                Index Cond: (customer_id = '1'::bigint)
(7 rows)

預先載入可能會在幕後觸發多個查詢,並且某些查詢可能需要先前查詢的結果。因此,explain 實際上會執行查詢,然後要求查詢計畫。例如,執行

Customer.where(id: 1).includes(:orders).explain

對於 MySQL 和 MariaDB,可能會產生此結果

EXPLAIN SELECT `customers`.* FROM `customers`  WHERE `customers`.`id` = 1
+----+-------------+-----------+-------+---------------+
| id | select_type | table     | type  | possible_keys |
+----+-------------+-----------+-------+---------------+
|  1 | SIMPLE      | customers | const | PRIMARY       |
+----+-------------+-----------+-------+---------------+
+---------+---------+-------+------+-------+
| key     | key_len | ref   | rows | Extra |
+---------+---------+-------+------+-------+
| PRIMARY | 4       | const |    1 |       |
+---------+---------+-------+------+-------+

1 row in set (0.00 sec)

EXPLAIN SELECT `orders`.* FROM `orders`  WHERE `orders`.`customer_id` IN (1)
+----+-------------+--------+------+---------------+
| id | select_type | table  | type | possible_keys |
+----+-------------+--------+------+---------------+
|  1 | SIMPLE      | orders | ALL  | NULL          |
+----+-------------+--------+------+---------------+
+------+---------+------+------+-------------+
| key  | key_len | ref  | rows | Extra       |
+------+---------+------+------+-------------+
| NULL | NULL    | NULL |    1 | Using where |
+------+---------+------+------+-------------+


1 row in set (0.00 sec)

對於 PostgreSQL,可能會產生此結果

  Customer Load (0.3ms)  SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1  [["id", 1]]
  Order Load (0.3ms)  SELECT "orders".* FROM "orders" WHERE "orders"."customer_id" = $1  [["customer_id", 1]]
=> EXPLAIN SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 [["id", 1]]
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Scan using customers_pkey on customers  (cost=0.15..8.17 rows=1 width=164)
   Index Cond: (id = '1'::bigint)
(2 rows)

22.1 Explain 選項

對於支援它們的資料庫和配接器(目前為 PostgreSQL、MySQL 和 MariaDB),可以傳遞選項以提供更深入的分析。

使用 PostgreSQL,以下程式碼

Customer.where(id: 1).joins(:orders).explain(:analyze, :verbose)

會產生

EXPLAIN (ANALYZE, VERBOSE) SELECT "shop_accounts".* FROM "shop_accounts" INNER JOIN "customers" ON "customers"."id" = "shop_accounts"."customer_id" WHERE "shop_accounts"."id" = $1 [["id", 1]]
                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.30..16.37 rows=1 width=24) (actual time=0.003..0.004 rows=0 loops=1)
   Output: shop_accounts.id, shop_accounts.customer_id, shop_accounts.customer_carrier_id
   Inner Unique: true
   ->  Index Scan using shop_accounts_pkey on public.shop_accounts  (cost=0.15..8.17 rows=1 width=24) (actual time=0.003..0.003 rows=0 loops=1)
         Output: shop_accounts.id, shop_accounts.customer_id, shop_accounts.customer_carrier_id
         Index Cond: (shop_accounts.id = '1'::bigint)
   ->  Index Only Scan using customers_pkey on public.customers  (cost=0.15..8.17 rows=1 width=8) (never executed)
         Output: customers.id
         Index Cond: (customers.id = shop_accounts.customer_id)
         Heap Fetches: 0
 Planning Time: 0.063 ms
 Execution Time: 0.011 ms
(12 rows)

使用 MySQL 或 MariaDB,以下程式碼

Customer.where(id: 1).joins(:orders).explain(:analyze)

會產生

ANALYZE SELECT `shop_accounts`.* FROM `shop_accounts` INNER JOIN `customers` ON `customers`.`id` = `shop_accounts`.`customer_id` WHERE `shop_accounts`.`id` = 1
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | r_rows | filtered | r_filtered | Extra                          |
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
|  1 | SIMPLE      | NULL  | NULL | NULL          | NULL | NULL    | NULL | NULL | NULL   | NULL     | NULL       | no matching row in const table |
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
1 row in set (0.00 sec)

EXPLAIN 和 ANALYZE 選項在不同的 MySQL 和 MariaDB 版本中有所不同。(MySQL 5.7MySQL 8.0MariaDB

22.2 解讀 EXPLAIN

解讀 EXPLAIN 的輸出已超出本指南的範圍。以下提示可能會有所幫助



回到頂端