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
,且不會引發例外。
如果您的 預設範圍 包含一個排序方法,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
如果您的 預設範圍 包含一個排序方法,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
不同,後者傳入的 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
預設情況下,記錄會以主鍵遞增順序擷取。:start
選項允許您設定序列的第一個 ID,只要最小的 ID 不是您需要的 ID。例如,如果您想要繼續中斷的批次處理程序,只要您將最後處理的 ID 儲存為檢查點,這將很有用。
例如,要僅寄送電子報給主鍵從 2000 開始的客戶
Customer.find_each(start: 2000) do |customer|
NewsMailer.weekly(customer).deliver_now
end
:finish
與 :start
選項類似,:finish
允許您設定序列的最後一個 ID,只要最大的 ID 不是您需要的 ID。例如,如果您想要使用基於 :start
和 :finish
的記錄子集來執行批次處理程序,這將很有用。
例如,要僅寄送電子報給主鍵從 2000 到 10000 的客戶
Customer.find_each(start: 2000, finish: 10000) do |customer|
NewsMailer.weekly(customer).deliver_now
end
另一個範例是,如果您想要多個工作者處理相同的處理佇列。您可以讓每個工作者處理 10000 筆記錄,方法是在每個工作者上設定適當的 :start
和 :finish
選項。
: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'")
。這將找到所有標題欄位值為「演算法導論」的書。
將您自己的條件建置為純字串可能會讓您容易受到 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 條件
NOT
SQL 查詢可以透過 where.not
建立
Customer.where.not(orders_count: [1, 3, 5])
換句話說,這個查詢可以透過呼叫沒有參數的 where
,然後立即與傳遞 where
條件的 not
連結來產生。這會產生類似這樣的 SQL
SELECT * FROM customers WHERE (customers.orders_count NOT IN (1,3,5))
如果一個查詢有一個在可為空欄位上具有非 nil 值的雜湊條件,則在可為空欄位上具有 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 條件
AND
條件可以透過連結 where
條件來建立。
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
在大部分資料庫系統中,使用 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
套用至 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)
將傳回從第 31 個開始的最多 5 個客戶。SQL 如下所示
SELECT * FROM customers LIMIT 5 OFFSET 30
7 分組
若要將 GROUP BY
子句套用至查詢器所觸發的 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
方法加入查詢,將 HAVING
子句加入 Model.find
所觸發的 SQL。
例如
Order.select("created_at, 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 在關聯上提供 readonly
方法,用來明確禁止修改任何傳回的物件。任何嘗試變更唯讀記錄的動作都不會成功,並會引發 ActiveRecord::ReadOnlyRecord
例外。
customer = Customer.readonly.first
customer.visits += 1
customer.save # Raises an ActiveRecord::ReadOnlyRecord
由於 customer
明確設定為唯讀物件,因此當使用更新後的 visits 值呼叫 customer.save
時,上述程式碼會引發 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
上述工作階段會產生以下適用於 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 本書並印出其作者的 last_name
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 允許您使用陣列、雜湊或陣列/雜湊的巢狀雜湊,透過單一的 Model.find
呼叫,熱切載入任意數量的關聯,方法是使用 includes
方法。
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),聯結條件必須相符,否則不會傳回任何記錄。
如果一個關聯關係在聯結中作為 eager loaded,任何自訂選取子句的欄位將不會出現在載入的模型中。這是因為它們應該出現在父記錄或子記錄上,這點很模糊。
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
上述程式碼將只執行 2 個查詢,與原始案例中的 11 個查詢相反
SELECT DISTINCT books.id FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id LIMIT 10
SELECT books.id AS t0_r0, books.last_name AS t0_r1, ...
FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id
WHERE books.id IN (1,2,3,4,5,6,7,8,9,10)
eager_load
方法使用陣列、雜湊或陣列/雜湊的巢狀雜湊,就像 includes
方法一樣,使用單一 Model.find
呼叫來載入任意數量的關聯關係。此外,就像 includes
方法一樣,你可以為 eager loaded 的關聯關係指定條件。
13.5 strict_loading
Eager loading 可以防止 N + 1 查詢,但你可能仍然會 lazy loading 一些關聯關係。若要確保沒有任何關聯關係是 lazy loaded 的,你可以啟用 strict_loading
。
在關聯關係上啟用嚴格載入模式後,如果記錄嘗試 lazy loading 任何關聯關係,將會引發 ActiveRecord::StrictLoadingViolationError
user = User.strict_loading.first
user.address.city # raises an ActiveRecord::StrictLoadingViolationError
user.comments.to_a # raises an ActiveRecord::StrictLoadingViolationError
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
14 範圍
範圍允許您指定常用查詢,這些查詢可以作為關聯物件或模型上的方法呼叫來參考。使用這些範圍,您可以使用先前涵蓋的每個方法,例如 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
有關列舉的完整文件可在此處找到 here.
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
一樣)。
假設我們要在建立新記錄時將「鎖定」屬性設定為 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
有個近親,稱為 connection.select_all
。select_all
會使用自訂 SQL 從資料庫中擷取物件,就像 find_by_sql
一樣,但不會實例化它們。這個方法會傳回 ActiveRecord::Result
類別的實例,而對這個物件呼叫 to_a
會傳回一個雜湊陣列,其中每個雜湊表示一筆記錄。
irb> Customer.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
可用於從目前關聯中選取指定欄位中的值。它接受一串欄位名稱作為引數,並傳回指定欄位值的第 1 列,其資料類型相符。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
可能會產生
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)
在 MySQL 和 MariaDB 中。
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),可以傳遞選項以提供更深入的分析。
使用 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
回饋
我們鼓勵您協助提升本指南的品質。
如果您發現任何錯字或事實錯誤,請協助我們修正。首先,您可以閱讀我們的文件貢獻章節。
您也可能發現不完整或過時的內容。請為 main 新增任何遺漏的文件。請務必先查看Edge Guides,以確認問題是否已在 main 分支中修正。查看Ruby on Rails 指南準則,以了解風格和慣例。
如果您發現需要修正的地方,但無法自行修補,請開啟問題。
最後,歡迎在官方 Ruby on Rails 論壇討論任何與 Ruby on Rails 文件相關的事項。