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
2 從資料庫擷取物件
為了從資料庫擷取物件,Active Record 提供了多種尋找器方法。每個尋找器方法都允許您傳遞參數來對您的資料庫執行某些查詢,而無需編寫原始 SQL。
這些方法包括
annotate
find
create_with
distinct
eager_load
extending
extract_associated
from
group
having
includes
joins
left_outer_joins
limit
lock
none
offset
optimizer_hints
order
preload
readonly
references
reorder
reselect
regroup
reverse_order
select
where
傳回集合的尋找器方法,例如 where
和 group
,會傳回 ActiveRecord::Relation
的執行個體。尋找單一實體的方法,例如 find
和 first
,會傳回模型的單一執行個體。
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_by
和 where
等方法中指定條件時,使用 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_by
和 where
等尋找方法使用。請參閱以下範例:
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_each
和 find_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_each
,batch_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")
您也可以指定 ASC
或 DESC
。
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")
在大多數資料庫系統中,當使用 select
、pluck
和 ids
等方法從結果集中選取具有 distinct
的欄位時;除非 order
子句中使用的欄位包含在選取清單中,否則 order
方法會引發 ActiveRecord::StatementInvalid
例外。請參閱下一節,瞭解如何從結果集中選取欄位。
5 選取特定欄位
預設情況下,Model.find
會使用 select *
從結果集中選取所有欄位。
若要僅從結果集中選取欄位的子集,您可以使用 select
方法指定子集。
例如,若要僅選取 isbn
和 out_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,您可以使用關係上的 limit
和 offset
方法指定 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
子句:joins
和 left_outer_joins
。 joins
應用於 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.associated
和 where.missing
associated
和 missing
查詢方法可讓您根據關聯是否存在來選取一組記錄。
要使用 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)
作用域允許您指定常用查詢,這些查詢可以作為關聯物件或模型上的方法呼叫來引用。使用這些作用域,您可以使用先前涵蓋的所有方法,例如 where
、joins
和 includes
。所有作用域主體都應該返回一個 ActiveRecord::Relation
或 nil
,以便可以在其上呼叫更多方法(例如其他作用域)。
若要定義一個簡單的作用域,我們在類別內部使用 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
我們可以混合和匹配 scope
和 where
條件,最終的 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
將在 scope
和 where
條件中前置。
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
正在 scope
和 where
條件中合併。
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_name
和 orders_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
時,您可以在語句中鏈接方法,例如 all
、where
和 joins
。傳回單一物件的方法(請參閱 擷取單一物件章節)必須位於語句的末尾。
以下有一些範例。本指南不會涵蓋所有可能性,僅提供一些範例。當呼叫 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_by
和 find_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_all
。select_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
可以用來從目前關係中選取指定欄位的數值。它接受一個欄位名稱列表作為參數,並返回指定欄位值的第一行,並帶有對應的資料型別。pick
是 relation.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
相同的查詢來查詢資料庫,但它會返回 true
或 false
,而不是返回物件或物件集合。
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)
22.2 解讀 EXPLAIN
解讀 EXPLAIN 的輸出已超出本指南的範圍。以下提示可能會有所幫助
SQLite3:EXPLAIN QUERY PLAN
MySQL:EXPLAIN 輸出格式
MariaDB:EXPLAIN
PostgreSQL:使用 EXPLAIN