v7.1.3.2
更多資訊,請至 rubyonrails.org: 更多 Ruby on Rails

Active Record 查詢介面

本指南介紹使用 Active Record 從資料庫擷取資料的不同方法。

閱讀本指南後,您將了解

1 什麼是 Active Record 查詢介面?

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

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

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

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

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

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

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

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

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

Diagram of all of the bookstore models

2 從資料庫擷取物件

若要從資料庫擷取物件,Active Record 提供了多個尋找方法。每個尋找方法都允許您傳入引數,以在資料庫上執行特定查詢,而無需撰寫原始 SQL。

這些方法為

會傳回集合的尋找方法(例如 `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_bywhere 等方法的條件時,使用 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_bywhere 等尋找方法使用。請參閱以下範例

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

2.2 批次擷取多個物件

我們通常需要反覆處理大量記錄,例如當我們向大量客戶寄送電子報或匯出資料時。

這看起來很簡單

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

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

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

find_eachfind_in_batches 方法旨在用於批次處理大量無法一次放入記憶體的記錄。如果您只需要迴圈處理一千筆記錄,則建議使用常規尋找方法。

2.2.1 find_each

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

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

此程序會重複執行,視需要擷取更多批次,直到處理完所有記錄為止。

find_each 會在模型類別上執行,如上所示,也會在關聯上執行

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

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

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

2.2.1.1 find_each 的選項

:batch_size

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

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

:start

預設情況下,記錄會以主鍵遞增順序擷取。: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")

你也可以指定 ASCDESC

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

或按多個欄位排序

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

如果你想多次呼叫 order,後續的順序將會附加到第一個順序

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

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

5 選取特定欄位

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

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

例如,若要僅選取 isbnout_of_print

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

此尋找呼叫所使用的 SQL 查詢將類似於

SELECT isbn, out_of_print FROM books

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

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

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

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

Customer.select(:last_name).distinct

這會產生類似這樣的 SQL

SELECT DISTINCT last_name FROM customers

您也可以移除唯一性約束

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

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

6 限制和偏移

若要將 LIMIT 套用至 Model.find 觸發的 SQL,您可以使用 limitoffset 方法在關聯中指定 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 子句:joinsleft_outer_joinsjoins 應使用於 INNER JOIN 或自訂查詢,而 left_outer_joins 則用於使用 LEFT OUTER JOIN 的查詢。

12.1 joins

有多種方法可以使用 joins 方法。

12.1.1 使用字串 SQL 片段

您只要提供指定 JOIN 子句的原始 SQL 給 joins

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

這將產生下列 SQL

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

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

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

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

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

這會產生

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

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

12.1.3 聯結多個關聯

Book.joins(:author, :reviews)

這會產生

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

或者用白話文來說:「傳回所有有作者且至少有一則評論的書籍」。再次請注意,有數則評論的書籍會顯示多次。

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

這會產生

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

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

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

這會產生

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

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

12.1.4 指定聯結表的條件

您可以使用一般的 陣列字串 條件來指定聯結表的條件。 雜湊條件 提供了指定聯結表條件的特殊語法

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

這會找出所有有昨天建立的訂單的客戶,使用 BETWEEN SQL 表達式來比較 created_at

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

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

對於更進階的條件或重複使用現有的命名範圍,可以使用 merge。首先,讓我們新增一個新的命名範圍到 Order 模型

class Order < ApplicationRecord
  belongs_to :customer

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

現在我們可以使用 merge 來合併 created_in_time_range 範圍

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

這會找出所有有昨天建立的訂單的客戶,再次使用 BETWEEN SQL 表達式。

12.2 left_outer_joins

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

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

產生

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

表示:「傳回所有客戶及其評論數,無論他們是否有任何評論」

12.3 where.associatedwhere.missing

associatedmissing 查詢方法讓您可以根據關聯的存在或不存在來選取一組記錄。

要使用 where.associated

Customer.where.associated(:reviews)

產生

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

表示:「傳回已撰寫至少一則評論的所有客戶」。

要使用 where.missing

Customer.where.missing(:reviews)

產生

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

表示:「傳回尚未撰寫任何評論的所有客戶」。

13 渴望載入關聯

渴望載入是使用盡可能少的查詢來載入 Model.find 傳回物件的關聯記錄的機制。

13.1 N + 1 查詢問題

考慮以下程式碼,它會找出 10 本書並印出其作者的 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 範圍

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

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

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

要呼叫這個 out_of_print 範圍,我們可以在類別上呼叫它

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

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

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

範圍也可以在範圍內進行鏈接

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

14.1 傳入參數

您的範圍可以接受參數

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

呼叫範圍就像呼叫類別方法一樣

irb> Book.costs_more_than(100.10)

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

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

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

irb> author.books.costs_more_than(100.10)

14.2 使用條件式

您的範圍可以使用條件式

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

與其他範例一樣,這將表現得類似於類別方法。

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

然而,有一個重要的警告:即使條件評估為 false,範圍也會永遠傳回 ActiveRecord::Relation 物件,而類別方法會傳回 nil。如果任何條件傳回 false,這會在將類別方法與條件串連時導致 NoMethodError

14.3 套用預設範圍

如果我們希望在所有對模型的查詢中套用範圍,我們可以在模型本身內使用 default_scope 方法。

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

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

SELECT * FROM books WHERE (out_of_print = false)

如果你需要在預設範圍中執行更複雜的事情,你可以選擇將它定義為類別方法

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

當範圍引數以 Hash 提供時,default_scope 也會在建立/建構記錄時套用。它不會在更新記錄時套用。例如:

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

請注意,當以 Array 格式提供時,default_scope 查詢引數無法轉換為 Hash 以進行預設屬性指定。例如:

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

14.4 範圍合併

就像 where 子句一樣,範圍會使用 AND 條件進行合併。

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

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

我們可以混合搭配 scopewhere 條件,而最終的 SQL 會將所有條件與 AND 結合。

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

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

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

一個重要的警告是,default_scope 會在 scopewhere 條件中預先附加。

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

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

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

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

正如你可以在上面看到的,default_scopescopewhere 條件中都被合併了。

14.5 移 除所有範圍

如果我們想移除範圍,無論出於何種原因,我們可以使用 unscoped 方法。如果在模型中指定了 default_scope,並且不應將其應用於此特定查詢,這將特別有用。

Book.unscoped.load

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

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

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

unscoped 也可以接受區塊

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

15 動態尋找器

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

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

如果你想要同時透過 first_nameorders_count 尋找,你可以透過在欄位之間輸入「and」來串連這些尋找器。例如,Customer.find_by_first_name_and_orders_count("Ryan", 5)

16 列舉

列舉讓你能夠為屬性定義一個值陣列,並透過名稱參照它們。儲存在資料庫中的實際值是一個整數,已對應到其中一個值。

宣告列舉會

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

針對列舉的所有可能值。

例如,給定此 enum 宣告

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

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

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

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

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

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

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

有關列舉的完整文件可在此處找到 here.

17 了解方法串接

Active Record 模式實作 方法串接,這讓我們得以在簡單且直接的方式中,同時使用多個 Active Record 方法。

當先前呼叫的方法傳回 ActiveRecord::Relation 時,您可以在陳述式中串接方法,例如 allwherejoins。傳回單一物件的方法(請參閱 擷取單一物件區段)必須在陳述式的結尾。

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

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

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

結果應類似如下

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

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

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

上述應該會產生

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

請注意,如果查詢符合多筆記錄,find_by 只會擷取第一筆並忽略其他筆(請參閱上述 LIMIT 1 陳述式)。

18 尋找或建立新物件

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

18.1 find_or_create_by

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

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

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

此方法產生的 SQL 如下所示

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

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

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

假設我們要在建立新記錄時將「鎖定」屬性設定為 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_sqlfind_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_allselect_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 列,其資料類型相符。pickrelation.limit(1).pluck(*column_names).first 的簡寫,當您已有一個限制為一列的關聯時,這項功能特別有用。

pick 可讓您取代類似以下的程式碼

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

改為

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

19.4 ids

ids 可用於使用資料表的「主索引鍵」選取關聯的所有 ID。

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

20 物件是否存在

如果您只想檢查物件是否存在,有一個方法稱為 exists?。此方法會使用與 find 相同的查詢來查詢資料庫,但它不會傳回物件或物件集合,而是會傳回 truefalse

Customer.exists?(1)

exists? 方法也接受多個值,但重點是,只要其中任何一筆記錄存在,它就會傳回 true

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

甚至可以在模型或關聯上使用 exists?,而無需任何引數。

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

如果至少有一位客戶的 first_name 為「Ryan」,則上述程式碼會傳回 true,否則傳回 false

Customer.exists?

如果 customers 資料表為空,則上述程式碼會傳回 false,否則傳回 true

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

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

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

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

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

21 計算

本節使用 count 作為本前言中的範例方法,但所描述的選項適用於所有子節。

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

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

或在關係上運作

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

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

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

將會執行

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

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

21.1 count

如果您想查看模型的表格中有多少記錄,您可以呼叫 Customer.count,它會傳回數量。如果您想更具體並找出資料庫中所有有標題的客戶,您可以使用 Customer.count(:title)

有關選項,請參閱父節 計算

21.2 average

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

Order.average("subtotal")

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

有關選項,請參閱父節 計算

21.3 minimum

如果您想找出表格中欄位的最小值,您可以呼叫與表格相關的類別上的 minimum 方法。此方法呼叫看起來會像這樣

Order.minimum("subtotal")

有關選項,請參閱父節 計算

21.4 maximum

如果您想找出表格中欄位的最大值,您可以呼叫與表格相關的類別上的 maximum 方法。此方法呼叫看起來會像這樣

Order.maximum("subtotal")

有關選項,請參閱父節 計算

21.5 sum

如果您想找出資料表中所有記錄的欄位總和,您可以呼叫與資料表相關的類別上的 sum 方法。此方法呼叫看起來會像這樣

Order.sum("subtotal")

有關選項,請參閱父節 計算

22 執行 EXPLAIN

您可以在關聯上執行 explain。EXPLAIN 輸出會因資料庫而異。

例如,執行

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

可能會產生

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)

EXPLAIN 和 ANALYZE 選項會因 MySQL 和 MariaDB 版本而異。(MySQL 5.7MySQL 8.0MariaDB

22.2 解釋 EXPLAIN

EXPLAIN 輸出的解釋超出了本指南的範圍。下列指標可能會有幫助

回饋

我們鼓勵您協助提升本指南的品質。

如果您發現任何錯字或事實錯誤,請協助我們修正。首先,您可以閱讀我們的文件貢獻章節。

您也可能發現不完整或過時的內容。請為 main 新增任何遺漏的文件。請務必先查看Edge Guides,以確認問題是否已在 main 分支中修正。查看Ruby on Rails 指南準則,以了解風格和慣例。

如果您發現需要修正的地方,但無法自行修補,請開啟問題

最後,歡迎在官方 Ruby on Rails 論壇討論任何與 Ruby on Rails 文件相關的事項。