# Active Record 查詢
本文介紹使用 Active Record 從數據庫中獲取數據的不同方法。
讀完本文,你將學到:
* 如何使用各種方法查找滿足條件的記錄;
* 如何指定查找記錄的排序方式,獲取哪些屬性,分組等;
* 獲取數據時如何使用按需加載介紹數據庫查詢數;
* 如何使用動態查詢方法;
* 如何檢查某個記錄是否存在;
* 如何在 Active Record 模型中做各種計算;
* 如何執行 EXPLAIN 命令;
### Chapters
1. [從數據庫中獲取對象](#%E4%BB%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E8%8E%B7%E5%8F%96%E5%AF%B9%E8%B1%A1)
* [獲取單個對象](#%E8%8E%B7%E5%8F%96%E5%8D%95%E4%B8%AA%E5%AF%B9%E8%B1%A1)
* [獲取多個對象](#%E8%8E%B7%E5%8F%96%E5%A4%9A%E4%B8%AA%E5%AF%B9%E8%B1%A1)
* [批量獲取多個對象](#%E6%89%B9%E9%87%8F%E8%8E%B7%E5%8F%96%E5%A4%9A%E4%B8%AA%E5%AF%B9%E8%B1%A1)
2. [條件查詢](#%E6%9D%A1%E4%BB%B6%E6%9F%A5%E8%AF%A2)
* [純字符串條件](#%E7%BA%AF%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%9D%A1%E4%BB%B6)
* [數組條件](#%E6%95%B0%E7%BB%84%E6%9D%A1%E4%BB%B6)
* [Hash 條件](#hash-%E6%9D%A1%E4%BB%B6)
* [`NOT` 條件](#not-%E6%9D%A1%E4%BB%B6)
3. [排序](#%E6%8E%92%E5%BA%8F)
4. [查詢指定字段](#%E6%9F%A5%E8%AF%A2%E6%8C%87%E5%AE%9A%E5%AD%97%E6%AE%B5)
5. [限量和偏移](#%E9%99%90%E9%87%8F%E5%92%8C%E5%81%8F%E7%A7%BB)
6. [分組](#%E5%88%86%E7%BB%84)
7. [分組篩選](#%E5%88%86%E7%BB%84%E7%AD%9B%E9%80%89)
8. [條件覆蓋](#%E6%9D%A1%E4%BB%B6%E8%A6%86%E7%9B%96)
* [`unscope`](#unscope)
* [`only`](#only)
* [`reorder`](#reorder)
* [`reverse_order`](#reverse_order)
* [`rewhere`](#rewhere)
9. [空關系](#%E7%A9%BA%E5%85%B3%E7%B3%BB)
10. [只讀對象](#%E5%8F%AA%E8%AF%BB%E5%AF%B9%E8%B1%A1)
11. [更新時鎖定記錄](#%E6%9B%B4%E6%96%B0%E6%97%B6%E9%94%81%E5%AE%9A%E8%AE%B0%E5%BD%95)
* [樂觀鎖定](#%E4%B9%90%E8%A7%82%E9%94%81%E5%AE%9A)
* [悲觀鎖定](#%E6%82%B2%E8%A7%82%E9%94%81%E5%AE%9A)
12. [連接數據表](#%E8%BF%9E%E6%8E%A5%E6%95%B0%E6%8D%AE%E8%A1%A8)
* [使用字符串形式的 SQL 語句](#%E4%BD%BF%E7%94%A8%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%BD%A2%E5%BC%8F%E7%9A%84-sql-%E8%AF%AD%E5%8F%A5)
* [使用數組或 Hash 指定具名關聯](#%E4%BD%BF%E7%94%A8%E6%95%B0%E7%BB%84%E6%88%96-hash-%E6%8C%87%E5%AE%9A%E5%85%B7%E5%90%8D%E5%85%B3%E8%81%94)
* [指定用于連接數據表上的條件](#%E6%8C%87%E5%AE%9A%E7%94%A8%E4%BA%8E%E8%BF%9E%E6%8E%A5%E6%95%B0%E6%8D%AE%E8%A1%A8%E4%B8%8A%E7%9A%84%E6%9D%A1%E4%BB%B6)
13. [按需加載關聯](#%E6%8C%89%E9%9C%80%E5%8A%A0%E8%BD%BD%E5%85%B3%E8%81%94)
* [按需加載多個關聯](#%E6%8C%89%E9%9C%80%E5%8A%A0%E8%BD%BD%E5%A4%9A%E4%B8%AA%E5%85%B3%E8%81%94)
* [指定用于按需加載關聯上的條件](#%E6%8C%87%E5%AE%9A%E7%94%A8%E4%BA%8E%E6%8C%89%E9%9C%80%E5%8A%A0%E8%BD%BD%E5%85%B3%E8%81%94%E4%B8%8A%E7%9A%84%E6%9D%A1%E4%BB%B6)
14. [作用域](#%E4%BD%9C%E7%94%A8%E5%9F%9F)
* [傳入參數](#%E4%BC%A0%E5%85%A5%E5%8F%82%E6%95%B0)
* [合并作用域](#%E5%90%88%E5%B9%B6%E4%BD%9C%E7%94%A8%E5%9F%9F)
* [指定默認作用域](#%E6%8C%87%E5%AE%9A%E9%BB%98%E8%AE%A4%E4%BD%9C%E7%94%A8%E5%9F%9F)
* [刪除所有作用域](#%E5%88%A0%E9%99%A4%E6%89%80%E6%9C%89%E4%BD%9C%E7%94%A8%E5%9F%9F)
15. [動態查詢方法](#%E5%8A%A8%E6%80%81%E6%9F%A5%E8%AF%A2%E6%96%B9%E6%B3%95)
16. [查找或構建新對象](#%E6%9F%A5%E6%89%BE%E6%88%96%E6%9E%84%E5%BB%BA%E6%96%B0%E5%AF%B9%E8%B1%A1)
* [`find_or_create_by`](#find_or_create_by)
* [`find_or_create_by!`](#find_or_create_by-bang)
* [`find_or_initialize_by`](#find_or_initialize_by)
17. [使用 SQL 語句查詢](#%E4%BD%BF%E7%94%A8-sql-%E8%AF%AD%E5%8F%A5%E6%9F%A5%E8%AF%A2)
* [`select_all`](#select_all)
* [`pluck`](#pluck)
* [`ids`](#ids)
18. [檢查對象是否存在](#%E6%A3%80%E6%9F%A5%E5%AF%B9%E8%B1%A1%E6%98%AF%E5%90%A6%E5%AD%98%E5%9C%A8)
19. [計算](#%E8%AE%A1%E7%AE%97)
* [計數](#%E8%AE%A1%E6%95%B0)
* [平均值](#%E5%B9%B3%E5%9D%87%E5%80%BC)
* [最小值](#%E6%9C%80%E5%B0%8F%E5%80%BC)
* [最大值](#%E6%9C%80%E5%A4%A7%E5%80%BC)
* [求和](#%E6%B1%82%E5%92%8C)
20. [執行 EXPLAIN 命令](#%E6%89%A7%E8%A1%8C-explain-%E5%91%BD%E4%BB%A4)
* [解讀 EXPLAIN 命令的輸出結果](#%E8%A7%A3%E8%AF%BB-explain-%E5%91%BD%E4%BB%A4%E7%9A%84%E8%BE%93%E5%87%BA%E7%BB%93%E6%9E%9C)
如果習慣使用 SQL 查詢數據庫,會發現在 Rails 中執行相同的查詢有更好的方式。大多數情況下,在 Active Record 中無需直接使用 SQL。
文中的實例代碼會用到下面一個或多個模型:
下面所有的模型除非有特別說明之外,都使用 `id` 做主鍵。
```
class Client < ActiveRecord::Base
has_one :address
has_many :orders
has_and_belongs_to_many :roles
end
```
```
class Address < ActiveRecord::Base
belongs_to :client
end
```
```
class Order < ActiveRecord::Base
belongs_to :client, counter_cache: true
end
```
```
class Role < ActiveRecord::Base
has_and_belongs_to_many :clients
end
```
Active Record 會代你執行數據庫查詢,可以兼容大多數數據庫(MySQL,PostgreSQL 和 SQLite 等)。不管使用哪種數據庫,所用的 Active Record 方法都是一樣的。
### 1 從數據庫中獲取對象
Active Record 提供了很多查詢方法,用來從數據庫中獲取對象。每個查詢方法都接可接受參數,不用直接寫 SQL 就能在數據庫中執行指定的查詢。
這些方法是:
* `find`
* `create_with`
* `distinct`
* `eager_load`
* `extending`
* `from`
* `group`
* `having`
* `includes`
* `joins`
* `limit`
* `lock`
* `none`
* `offset`
* `order`
* `preload`
* `readonly`
* `references`
* `reorder`
* `reverse_order`
* `select`
* `uniq`
* `where`
上述所有方法都返回一個 `ActiveRecord::Relation` 實例。
`Model.find(options)` 方法執行的主要操作概括如下:
* 把指定的選項轉換成等價的 SQL 查詢語句;
* 執行 SQL 查詢,從數據庫中獲取結果;
* 為每個查詢結果實例化一個對應的模型對象;
* 如果有 `after_find` 回調,再執行 `after_find` 回調;
#### 1.1 獲取單個對象
在 Active Record 中獲取單個對象有好幾種方法。
##### 1.1.1 使用主鍵
使用 `Model.find(primary_key)` 方法可以獲取指定主鍵對應的對象。例如:
```
# Find the client with primary key (id) 10.
client = Client.find(10)
# => #<Client id: 10, first_name: "Ryan">
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
```
如果未找到匹配的記錄,`Model.find(primary_key)` 會拋出 `ActiveRecord::RecordNotFound` 異常。
##### 1.1.2 `take`
`Model.take` 方法會獲取一個記錄,不考慮任何順序。例如:
```
client = Client.take
# => #<Client id: 1, first_name: "Lifo">
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients LIMIT 1
```
如果沒找到記錄,`Model.take` 不會拋出異常,而是返回 `nil`。
獲取的記錄根據所用的數據庫引擎會有所不同。
##### 1.1.3 `first`
`Model.first` 獲取按主鍵排序得到的第一個記錄。例如:
```
client = Client.first
# => #<Client id: 1, first_name: "Lifo">
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
```
`Model.first` 如果沒找到匹配的記錄,不會拋出異常,而是返回 `nil`。
##### 1.1.4 `last`
`Model.last` 獲取按主鍵排序得到的最后一個記錄。例如:
```
client = Client.last
# => #<Client id: 221, first_name: "Russel">
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
```
`Model.last` 如果沒找到匹配的記錄,不會拋出異常,而是返回 `nil`。
##### 1.1.5 `find_by`
`Model.find_by` 獲取滿足條件的第一個記錄。例如:
```
Client.find_by first_name: 'Lifo'
# => #<Client id: 1, first_name: "Lifo">
Client.find_by first_name: 'Jon'
# => nil
```
等價于:
```
Client.where(first_name: 'Lifo').take
```
##### 1.1.6 `take!`
`Model.take!` 方法會獲取一個記錄,不考慮任何順序。例如:
```
client = Client.take!
# => #<Client id: 1, first_name: "Lifo">
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients LIMIT 1
```
如果未找到匹配的記錄,`Model.take!` 會拋出 `ActiveRecord::RecordNotFound` 異常。
##### 1.1.7 `first!`
`Model.first!` 獲取按主鍵排序得到的第一個記錄。例如:
```
client = Client.first!
# => #<Client id: 1, first_name: "Lifo">
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
```
如果未找到匹配的記錄,`Model.first!` 會拋出 `ActiveRecord::RecordNotFound` 異常。
##### 1.1.8 `last!`
`Model.last!` 獲取按主鍵排序得到的最后一個記錄。例如:
```
client = Client.last!
# => #<Client id: 221, first_name: "Russel">
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
```
如果未找到匹配的記錄,`Model.last!` 會拋出 `ActiveRecord::RecordNotFound` 異常。
##### 1.1.9 `find_by!`
`Model.find_by!` 獲取滿足條件的第一個記錄。如果沒找到匹配的記錄,會拋出 `ActiveRecord::RecordNotFound` 異常。例如:
```
Client.find_by! first_name: 'Lifo'
# => #<Client id: 1, first_name: "Lifo">
Client.find_by! first_name: 'Jon'
# => ActiveRecord::RecordNotFound
```
等價于:
```
Client.where(first_name: 'Lifo').take!
```
#### 1.2 獲取多個對象
##### 1.2.1 使用多個主鍵
`Model.find(array_of_primary_key)` 方法可接受一個由主鍵組成的數組,返回一個由主鍵對應記錄組成的數組。例如:
```
# Find the clients with primary keys 1 and 10.
client = Client.find([1, 10]) # Or even Client.find(1, 10)
# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
```
上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients WHERE (clients.id IN (1,10))
```
只要有一個主鍵的對應的記錄未找到,`Model.find(array_of_primary_key)` 方法就會拋出 `ActiveRecord::RecordNotFound` 異常。
##### 1.2.2 take
`Model.take(limit)` 方法獲取 `limit` 個記錄,不考慮任何順序:
```
Client.take(2)
# => [#<Client id: 1, first_name: "Lifo">,
#<Client id: 2, first_name: "Raf">]
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients LIMIT 2
```
##### 1.2.3 first
`Model.first(limit)` 方法獲取按主鍵排序的前 `limit` 個記錄:
```
Client.first(2)
# => [#<Client id: 1, first_name: "Lifo">,
#<Client id: 2, first_name: "Raf">]
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients ORDER BY id ASC LIMIT 2
```
##### 1.2.4 last
`Model.last(limit)` 方法獲取按主鍵降序排列的前 `limit` 個記錄:
```
Client.last(2)
# => [#<Client id: 10, first_name: "Ryan">,
#<Client id: 9, first_name: "John">]
```
和上述方法等價的 SQL 查詢是:
```
SELECT * FROM clients ORDER BY id DESC LIMIT 2
```
#### 1.3 批量獲取多個對象
我們經常需要遍歷由很多記錄組成的集合,例如給大量用戶發送郵件列表,或者導出數據。
我們可能會直接寫出如下的代碼:
```
# This is very inefficient when the users table has thousands of rows.
User.all.each do |user|
NewsLetter.weekly_deliver(user)
end
```
但這種方法在數據表很大時就有點不現實了,因為 `User.all.each` 會一次讀取整個數據表,一行記錄創建一個模型對象,然后把整個模型對象數組存入內存。如果記錄數非常多,可能會用完內存。
Rails 為了解決這種問題提供了兩個方法,把記錄分成幾個批次,不占用過多內存。第一個方法是 `find_each`,獲取一批記錄,然后分別把每個記錄傳入代碼塊。第二個方法是 `find_in_batches`,獲取一批記錄,然后把整批記錄作為數組傳入代碼塊。
`find_each` 和 `find_in_batches` 方法的目的是分批處理無法一次載入內存的巨量記錄。如果只想遍歷幾千個記錄,更推薦使用常規的查詢方法。
##### 1.3.1 `find_each`
`find_each` 方法獲取一批記錄,然后分別把每個記錄傳入代碼塊。在下面的例子中,`find_each` 獲取 1000 個記錄,然后把每個記錄傳入代碼塊,直到所有記錄都處理完為止:
```
User.find_each do |user|
NewsLetter.weekly_deliver(user)
end
```
###### 1.3.1.1 `find_each` 方法的選項
在 `find_each` 方法中可使用 `find` 方法的大多數選項,但不能使用 `:order` 和 `:limit`,因為這兩個選項是保留給 `find_each` 內部使用的。
`find_each` 方法還可使用另外兩個選項:`:batch_size` 和 `:start`。
**`:batch_size`**
`:batch_size` 選項指定在把各記錄傳入代碼塊之前,各批次獲取的記錄數量。例如,一個批次獲取 5000 個記錄:
```
User.find_each(batch_size: 5000) do |user|
NewsLetter.weekly_deliver(user)
end
```
**`:start`**
默認情況下,按主鍵的升序方式獲取記錄,其中主鍵的類型必須是整數。如果不想用最小的 ID,可以使用 `:start` 選項指定批次的起始 ID。例如,前面的批量處理中斷了,但保存了中斷時的 ID,就可以使用這個選項繼續處理。
例如,在有 5000 個記錄的批次中,只向主鍵大于 2000 的用戶發送郵件列表,可以這么做:
```
User.find_each(start: 2000, batch_size: 5000) do |user|
NewsLetter.weekly_deliver(user)
end
```
還有一個例子是,使用多個 worker 處理同一個進程隊列。如果需要每個 worker 處理 10000 個記錄,就可以在每個 worker 中設置相應的 `:start` 選項。
##### 1.3.2 `find_in_batches`
`find_in_batches` 方法和 `find_each` 類似,都獲取一批記錄。二者的不同點是,`find_in_batches` 把整批記錄作為一個數組傳入代碼塊,而不是單獨傳入各記錄。在下面的例子中,會把 1000 個單據一次性傳入代碼塊,讓代碼塊后面的程序處理剩下的單據:
```
# Give add_invoices an array of 1000 invoices at a time
Invoice.find_in_batches(include: :invoice_lines) do |invoices|
export.add_invoices(invoices)
end
```
`:include` 選項可以讓指定的關聯和模型一同加載。
###### 1.3.2.1 `find_in_batches` 方法的選項
`find_in_batches` 方法和 `find_each` 方法一樣,可以使用 `:batch_size` 和 `:start` 選項,還可使用常規的 `find` 方法中的大多數選項,但不能使用 `:order` 和 `:limit` 選項,因為這兩個選項保留給 `find_in_batches` 方法內部使用。
### 2 條件查詢
`where` 方法用來指定限制獲取記錄的條件,用于 SQL 語句的 `WHERE` 子句。條件可使用字符串、數組或 Hash 指定。
#### 2.1 純字符串條件
如果查詢時要使用條件,可以直接指定。例如 `Client.where("orders_count = '2'")`,獲取 `orders_count` 字段為 `2` 的客戶記錄。
使用純字符串指定條件可能導致 SQL 注入漏洞。例如,`Client.where("first_name LIKE '%#{params[:first_name]}%'")`,這里的條件就不安全。推薦使用的條件指定方式是數組,請閱讀下一節。
#### 2.2 數組條件
如果數字是在別處動態生成的話應該怎么處理呢?可用下面的查詢:
```
Client.where("orders_count = ?", params[:orders])
```
Active Record 會先處理第一個元素中的條件,然后使用后續元素替換第一個元素中的問號(`?`)。
指定多個條件的方式如下:
```
Client.where("orders_count = ? AND locked = ?", params[:orders], false)
```
在這個例子中,第一個問號會替換成 `params[:orders]` 的值;第二個問號會替換成 `false` 在 SQL 中對應的值,具體的值視所用的適配器而定。
下面這種形式
```
Client.where("orders_count = ?", params[:orders])
```
要比這種形式好
```
Client.where("orders_count = #{params[:orders]}")
```
因為前者傳入的參數更安全。直接在條件字符串中指定的條件會原封不動的傳給數據庫。也就是說,即使用戶不懷好意,條件也會轉義。如果這么做,整個數據庫就處在一個危險境地,只要用戶發現可以接觸數據庫,就能做任何想做的事。所以,千萬別直接在條件字符串中使用參數。
關于 SQL 注入更詳細的介紹,請閱讀“[Ruby on Rails 安全指南](security.html#sql-injection)”
##### 2.2.1 條件中的占位符
除了使用問號占位之外,在數組條件中還可使用鍵值對 Hash 形式的占位符:
```
Client.where("created_at >= :start_date AND created_at <= :end_date",
{start_date: params[:start_date], end_date: params[:end_date]})
```
如果條件中有很多參數,使用這種形式可讀性更高。
#### 2.3 Hash 條件
Active Record 還允許使用 Hash 條件,提高條件語句的可讀性。使用 Hash 條件時,傳入 Hash 的鍵是要設定條件的字段,值是要設定的條件。
在 Hash 條件中只能指定相等。范圍和子集這三種條件。
##### 2.3.1 相等
```
Client.where(locked: true)
```
字段的名字還可使用字符串表示:
```
Client.where('locked' => true)
```
在 `belongs_to` 關聯中,如果條件中的值是模型對象,可用關聯鍵表示。這種條件指定方式也可用于多態關聯。
```
Post.where(author: author)
Author.joins(:posts).where(posts: { author: author })
```
條件的值不能為 Symbol。例如,不能這么指定條件:`Client.where(status: :active)`。
##### 2.3.2 范圍
```
Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
```
指定這個條件后,會使用 SQL `BETWEEN` 子句查詢昨天創建的客戶:
```
SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
```
這段代碼演示了[數組條件](#array-conditions)的簡寫形式。
##### 2.3.3 子集
如果想使用 `IN` 子句查詢記錄,可以在 Hash 條件中使用數組:
```
Client.where(orders_count: [1,3,5])
```
上述代碼生成的 SQL 語句如下:
```
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
```
#### 2.4 `NOT` 條件
SQL `NOT` 查詢可用 `where.not` 方法構建。
```
Post.where.not(author: author)
```
也即是說,這個查詢首先調用沒有參數的 `where` 方法,然后再調用 `not` 方法。
### 3 排序
要想按照特定的順序從數據庫中獲取記錄,可以使用 `order` 方法。
例如,想按照 `created_at` 的升序方式獲取一些記錄,可以這么做:
```
Client.order(:created_at)
# OR
Client.order("created_at")
```
還可使用 `ASC` 或 `DESC` 指定排序方式:
```
Client.order(created_at: :desc)
# OR
Client.order(created_at: :asc)
# OR
Client.order("created_at DESC")
# OR
Client.order("created_at ASC")
```
或者使用多個字段排序:
```
Client.order(orders_count: :asc, created_at: :desc)
# OR
Client.order(:orders_count, created_at: :desc)
# OR
Client.order("orders_count ASC, created_at DESC")
# OR
Client.order("orders_count ASC", "created_at DESC")
```
如果想在不同的上下文中多次調用 `order`,可以在前一個 `order` 后再調用一次:
```
Client.order("orders_count ASC").order("created_at DESC")
# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
```
### 4 查詢指定字段
默認情況下,`Model.find` 使用 `SELECT *` 查詢所有字段。
要查詢部分字段,可使用 `select` 方法。
例如,只查詢 `viewable_by` 和 `locked` 字段:
```
Client.select("viewable_by, locked")
```
上述查詢使用的 SQL 語句如下:
```
SELECT viewable_by, locked FROM clients
```
使用時要注意,因為模型對象只會使用選擇的字段初始化。如果字段不能初始化模型對象,會得到以下異常:
```
ActiveModel::MissingAttributeError: missing attribute: <attribute>
```
其中 `<attribute>` 是所查詢的字段。`id` 字段不會拋出 `ActiveRecord::MissingAttributeError` 異常,所以在關聯中使用時要注意,因為關聯需要 `id` 字段才能正常使用。
如果查詢時希望指定字段的同值記錄只出現一次,可以使用 `distinct` 方法:
```
Client.select(:name).distinct
```
上述方法生成的 SQL 語句如下:
```
SELECT DISTINCT name FROM clients
```
查詢后還可以刪除唯一性限制:
```
query = Client.select(:name).distinct
# => Returns unique names
query.distinct(false)
# => Returns all names, even if there are duplicates
```
### 5 限量和偏移
要想在 `Model.find` 方法中使用 SQL `LIMIT` 子句,可使用 `limit` 和 `offset` 方法。
`limit` 方法指定獲取的記錄數量,`offset` 方法指定在返回結果之前跳過多少個記錄。例如:
```
Client.limit(5)
```
上述查詢最大只會返回 5 各客戶對象,因為沒指定偏移,多以會返回數據表中的前 5 個記錄。生成的 SQL 語句如下:
```
SELECT * FROM clients LIMIT 5
```
再加上 `offset` 方法:
```
Client.limit(5).offset(30)
```
這時會從第 31 個記錄開始,返回最多 5 個客戶對象。生成的 SQL 語句如下:
```
SELECT * FROM clients LIMIT 5 OFFSET 30
```
### 6 分組
要想在查詢時使用 SQL `GROUP BY` 子句,可以使用 `group` 方法。
例如,如果想獲取一組訂單的創建日期,可以這么做:
```
Order.select("date(created_at) as ordered_date, sum(price) as total_price").group("date(created_at)")
```
上述查詢會只會為相同日期下的訂單創建一個 `Order` 對象。
生成的 SQL 語句如下:
```
SELECT date(created_at) as ordered_date, sum(price) as total_price
FROM orders
GROUP BY date(created_at)
```
### 7 分組篩選
SQL 使用 `HAVING` 子句指定 `GROUP BY` 分組的條件。在 `Model.find` 方法中可使用 `:having` 選項指定 `HAVING` 子句。
例如:
```
Order.select("date(created_at) as ordered_date, sum(price) as total_price").
group("date(created_at)").having("sum(price) > ?", 100)
```
生成的 SQL 如下:
```
SELECT date(created_at) as ordered_date, sum(price) as total_price
FROM orders
GROUP BY date(created_at)
HAVING sum(price) > 100
```
這個查詢只會為同一天下的訂單創建一個 `Order` 對象,而且這一天的訂單總額要大于 $100。
### 8 條件覆蓋
#### 8.1 `unscope`
如果要刪除某個條件可使用 `unscope` 方法。例如:
```
Post.where('id > 10').limit(20).order('id asc').unscope(:order)
```
生成的 SQL 語句如下:
```
SELECT * FROM posts WHERE id > 10 LIMIT 20
# Original query without `unscope`
SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20
```
`unscope` 還可刪除 `WHERE` 子句中的條件。例如:
```
Post.where(id: 10, trashed: false).unscope(where: :id)
# SELECT "posts".* FROM "posts" WHERE trashed = 0
```
`unscope` 還可影響合并后的查詢:
```
Post.order('id asc').merge(Post.unscope(:order))
# SELECT "posts".* FROM "posts"
```
#### 8.2 `only`
查詢條件還可使用 `only` 方法覆蓋。例如:
```
Post.where('id > 10').limit(20).order('id desc').only(:order, :where)
```
執行的 SQL 語句如下:
```
SELECT * FROM posts WHERE id > 10 ORDER BY id DESC
# Original query without `only`
SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20
```
#### 8.3 `reorder`
`reorder` 方法覆蓋原來的 `order` 條件。例如:
```
class Post < ActiveRecord::Base
..
..
has_many :comments, -> { order('posted_at DESC') }
end
Post.find(10).comments.reorder('name')
```
執行的 SQL 語句如下:
```
SELECT * FROM posts WHERE id = 10 ORDER BY name
```
沒用 `reorder` 方法時執行的 SQL 語句如下:
```
SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC
```
#### 8.4 `reverse_order`
`reverse_order` 方法翻轉 `ORDER` 子句的條件。
```
Client.where("orders_count > 10").order(:name).reverse_order
```
執行的 SQL 語句如下:
```
SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC
```
如果查詢中沒有使用 `ORDER` 子句,`reverse_order` 方法會按照主鍵的逆序查詢:
```
Client.where("orders_count > 10").reverse_order
```
執行的 SQL 語句如下:
```
SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
```
這個方法**沒有**參數。
#### 8.5 `rewhere`
`rewhere` 方法覆蓋前面的 `where` 條件。例如:
```
Post.where(trashed: true).rewhere(trashed: false)
```
執行的 SQL 語句如下:
```
SELECT * FROM posts WHERE `trashed` = 0
```
如果不使用 `rewhere` 方法,寫成:
```
Post.where(trashed: true).where(trashed: false)
```
執行的 SQL 語句如下:
```
SELECT * FROM posts WHERE `trashed` = 1 AND `trashed` = 0
```
### 9 空關系
`none` 返回一個可鏈接的關系,沒有相應的記錄。`none` 方法返回對象的后續條件查詢,得到的還是空關系。如果想以可鏈接的方式響應可能無返回結果的方法或者作用域,可使用 `none` 方法。
```
Post.none # returns an empty Relation and fires no queries.
```
```
# The visible_posts method below is expected to return a Relation.
@posts = current_user.visible_posts.where(name: params[:name])
def visible_posts
case role
when 'Country Manager'
Post.where(country: country)
when 'Reviewer'
Post.published
when 'Bad User'
Post.none # => returning [] or nil breaks the caller code in this case
end
end
```
### 10 只讀對象
Active Record 提供了 `readonly` 方法,禁止修改獲取的對象。試圖修改只讀記錄的操作不會成功,而且會拋出 `ActiveRecord::ReadOnlyRecord` 異常。
```
client = Client.readonly.first
client.visits += 1
client.save
```
因為把 `client` 設為了只讀對象,所以上述代碼調用 `client.save` 方法修改 `visits` 的值時會拋出 `ActiveRecord::ReadOnlyRecord` 異常。
### 11 更新時鎖定記錄
鎖定可以避免更新記錄時的條件競爭,也能保證原子更新。
Active Record 提供了兩種鎖定機制:
* 樂觀鎖定
* 悲觀鎖定
#### 11.1 樂觀鎖定
樂觀鎖定允許多個用戶編輯同一個記錄,假設數據發生沖突的可能性最小。Rails 會檢查讀取記錄后是否有其他程序在修改這個記錄。如果檢測到有其他程序在修改,就會拋出 `ActiveRecord::StaleObjectError` 異常,忽略改動。
**樂觀鎖定字段**
為了使用樂觀鎖定,數據表中要有一個類型為整數的 `lock_version` 字段。每次更新記錄時,Active Record 都會增加 `lock_version` 字段的值。如果更新請求中的 `lock_version` 字段值比數據庫中的 `lock_version` 字段值小,會拋出 `ActiveRecord::StaleObjectError` 異常,更新失敗。例如:
```
c1 = Client.find(1)
c2 = Client.find(1)
c1.first_name = "Michael"
c1.save
c2.name = "should fail"
c2.save # Raises an ActiveRecord::StaleObjectError
```
拋出異常后,你要負責處理沖突,可以回滾操作、合并操作或者使用其他業務邏輯處理。
樂觀鎖定可以使用 `ActiveRecord::Base.lock_optimistically = false` 關閉。
要想修改 `lock_version` 字段的名字,可以使用 `ActiveRecord::Base` 提供的 `locking_column` 類方法:
```
class Client < ActiveRecord::Base
self.locking_column = :lock_client_column
end
```
#### 11.2 悲觀鎖定
悲觀鎖定使用底層數據庫提供的鎖定機制。使用 `lock` 方法構建的關系在所選記錄上生成一個“互斥鎖”(exclusive lock)。使用 `lock` 方法構建的關系一般都放入事務中,避免死鎖。
例如:
```
Item.transaction do
i = Item.lock.first
i.name = 'Jones'
i.save
end
```
在 MySQL 中,上述代碼生成的 SQL 如下:
```
SQL (0.2ms) BEGIN
Item Load (0.3ms) SELECT * FROM `items` LIMIT 1 FOR UPDATE
Item Update (0.4ms) UPDATE `items` SET `updated_at` = '2009-02-07 18:05:56', `name` = 'Jones' WHERE `id` = 1
SQL (0.8ms) COMMIT
```
`lock` 方法還可以接受 SQL 語句,使用其他鎖定類型。例如,MySQL 中有一個語句是 `LOCK IN SHARE MODE`,會鎖定記錄,但還是允許其他查詢讀取記錄。要想使用這個語句,直接傳入 `lock` 方法即可:
```
Item.transaction do
i = Item.lock("LOCK IN SHARE MODE").find(1)
i.increment!(:views)
end
```
如果已經創建了模型實例,可以在事務中加上這種鎖定,如下所示:
```
item = Item.first
item.with_lock do
# This block is called within a transaction,
# item is already locked.
item.increment!(:views)
end
```
### 12 連接數據表
Active Record 提供了一個查詢方法名為 `joins`,用來指定 SQL `JOIN` 子句。`joins` 方法的用法有很多種。
#### 12.1 使用字符串形式的 SQL 語句
在 `joins` 方法中可以直接使用 `JOIN` 子句的 SQL:
```
Client.joins('LEFT OUTER JOIN addresses ON addresses.client_id = clients.id')
```
生成的 SQL 語句如下:
```
SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id = clients.id
```
#### 12.2 使用數組或 Hash 指定具名關聯
這種方法只用于 `INNER JOIN`。
使用 `joins` 方法時,可以使用聲明[關聯](association_basics.html)時使用的關聯名指定 `JOIN` 子句。
例如,假如按照如下方式定義 `Category`、`Post`、`Comment`、`Guest` 和 `Tag` 模型:
```
class Category < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :category
has_many :comments
has_many :tags
end
class Comment < ActiveRecord::Base
belongs_to :post
has_one :guest
end
class Guest < ActiveRecord::Base
belongs_to :comment
end
class Tag < ActiveRecord::Base
belongs_to :post
end
```
下面各種用法能都使用 `INNER JOIN` 子句生成正確的連接查詢:
##### 12.2.1 連接單個關聯
```
Category.joins(:posts)
```
生成的 SQL 語句如下:
```
SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
```
用人類語言表達,上述查詢的意思是,“使用文章的分類創建分類對象”。注意,分類對象可能有重復,因為多篇文章可能屬于同一分類。如果不想出現重復,可使用 `Category.joins(:posts).uniq` 方法。
##### 12.2.2 連接多個關聯
```
Post.joins(:category, :comments)
```
生成的 SQL 語句如下:
```
SELECT posts.* FROM posts
INNER JOIN categories ON posts.category_id = categories.id
INNER JOIN comments ON comments.post_id = posts.id
```
用人類語言表達,上述查詢的意思是,“返回指定分類且至少有一個評論的所有文章”。注意,如果文章有多個評論,同個文章對象會出現多次。
##### 12.2.3 連接一層嵌套關聯
```
Post.joins(comments: :guest)
```
生成的 SQL 語句如下:
```
SELECT posts.* FROM posts
INNER JOIN comments ON comments.post_id = posts.id
INNER JOIN guests ON guests.comment_id = comments.id
```
用人類語言表達,上述查詢的意思是,“返回有一個游客發布評論的所有文章”。
##### 12.2.4 連接多層嵌套關聯
```
Category.joins(posts: [{ comments: :guest }, :tags])
```
生成的 SQL 語句如下:
```
SELECT categories.* FROM categories
INNER JOIN posts ON posts.category_id = categories.id
INNER JOIN comments ON comments.post_id = posts.id
INNER JOIN guests ON guests.comment_id = comments.id
INNER JOIN tags ON tags.post_id = posts.id
```
#### 12.3 指定用于連接數據表上的條件
作用在連接數據表上的條件可以使用[數組](#array-conditions)和[字符串](#pure-string-conditions)指定。[Hash 形式的條件]((#hash-conditions)使用的句法有點特殊:
```
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where('orders.created_at' => time_range)
```
還有一種更簡潔的句法是使用嵌套 Hash:
```
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Client.joins(:orders).where(orders: { created_at: time_range })
```
上述查詢會獲取昨天下訂單的所有客戶對象,再次用到了 SQL `BETWEEN` 語句。
### 13 按需加載關聯
使用 `Model.find` 方法獲取對象的關聯記錄時,按需加載機制會使用盡量少的查詢次數。
**N + 1 查詢問題**
假設有如下的代碼,獲取 10 個客戶對象,并把客戶的郵編打印出來
```
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
```
上述代碼初看起來很好,但問題在于查詢的總次數。上述代碼總共會執行 1(獲取 10 個客戶記錄)+ 10(分別獲取 10 個客戶的地址)= _11_ 次查詢。
**N + 1 查詢的解決辦法**
在 Active Record 中可以進一步指定要加載的所有關聯,調用 `Model.find` 方法是使用 `includes` 方法實現。使用 `includes` 后,Active Record 會使用盡可能少的查詢次數加載所有指定的關聯。
我們可以使用按需加載機制加載客戶的地址,把 `Client.limit(10)` 改寫成:
```
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
end
```
和前面的 **11** 次查詢不同,上述代碼只會執行 **2** 次查詢:
```
SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
```
#### 13.1 按需加載多個關聯
調用 `Model.find` 方法時,使用 `includes` 方法可以一次加載任意數量的關聯,加載的關聯可以通過數組、Hash、嵌套 Hash 指定。
##### 13.1.1 用數組指定多個關聯
```
Post.includes(:category, :comments)
```
上述代碼會加載所有文章,以及和每篇文章關聯的分類和評論。
##### 13.1.2 使用 Hash 指定嵌套關聯
```
Category.includes(posts: [{ comments: :guest }, :tags]).find(1)
```
上述代碼會獲取 ID 為 1 的分類,按需加載所有關聯的文章,文章的標簽和評論,以及每個評論的 `guest` 關聯。
#### 13.2 指定用于按需加載關聯上的條件
雖然 Active Record 允許使用 `joins` 方法指定用于按需加載關聯上的條件,但是推薦的做法是使用[連接數據表](#joining-tables)。
如果非要這么做,可以按照常規方式使用 `where` 方法。
```
Post.includes(:comments).where("comments.visible" => true)
```
上述代碼生成的查詢中會包含 `LEFT OUTER JOIN` 子句,而 `joins` 方法生成的查詢使用的是 `INNER JOIN` 子句。
```
SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible = 1)
```
如果沒指定 `where` 條件,上述代碼會生成兩個查詢語句。
如果像上面的代碼一樣使用 `includes`,即使所有文章都沒有評論,也會加載所有文章。使用 `joins` 方法(`INNER JOIN`)時,必須滿足連接條件,否則不會得到任何記錄。
### 14 作用域
作用域把常用的查詢定義成方法,在關聯對象或模型上調用。在作用域中可以使用前面介紹的所有方法,例如 `where`、`joins` 和 `includes`。所有作用域方法都會返回一個 `ActiveRecord::Relation` 對象,允許繼續調用其他方法(例如另一個作用域方法)。
要想定義簡單的作用域,可在類中調用 `scope` 方法,傳入執行作用域時運行的代碼:
```
class Post < ActiveRecord::Base
scope :published, -> { where(published: true) }
end
```
上述方式和直接定義類方法的作用一樣,使用哪種方式只是個人喜好:
```
class Post < ActiveRecord::Base
def self.published
where(published: true)
end
end
```
作用域可以鏈在一起調用:
```
class Post < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
```
可以在模型類上調用 `published` 作用域:
```
Post.published # => [published posts]
```
也可以在包含 `Post` 對象的關聯上調用:
```
category = Category.first
category.posts.published # => [published posts belonging to this category]
```
#### 14.1 傳入參數
作用域可接受參數:
```
class Post < ActiveRecord::Base
scope :created_before, ->(time) { where("created_at < ?", time) }
end
```
作用域的調用方法和類方法一樣:
```
Post.created_before(Time.zone.now)
```
不過這就和類方法的作用一樣了。
```
class Post < ActiveRecord::Base
def self.created_before(time)
where("created_at < ?", time)
end
end
```
如果作用域要接受參數,推薦直接使用類方法。有參數的作用域也可在關聯對象上調用:
```
category.posts.created_before(time)
```
#### 14.2 合并作用域
和 `where` 方法一樣,作用域也可通過 `AND` 合并查詢條件:
```
class User < ActiveRecord::Base
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
end
User.active.inactive
# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'inactive'
```
作用域還可以 `where` 一起使用,生成的 SQL 語句會使用 `AND` 連接所有條件。
```
User.active.where(state: 'finished')
# SELECT "users".* FROM "users" WHERE "users"."state" = 'active' AND "users"."state" = 'finished'
```
如果不想讓最后一個 `WHERE` 子句獲得優先權,可以使用 `Relation#merge` 方法。
```
User.active.merge(User.inactive)
# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
```
使用作用域時要注意,`default_scope` 會添加到作用域和 `where` 方法指定的條件之前。
```
class User < ActiveRecord::Base
default_scope { where state: 'pending' }
scope :active, -> { where state: 'active' }
scope :inactive, -> { where state: 'inactive' }
end
User.all
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
User.active
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
User.where(state: 'inactive')
# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
```
如上所示,`default_scope` 中的條件添加到了 `active` 和 `where` 之前。
#### 14.3 指定默認作用域
如果某個作用域要用在模型的所有查詢中,可以在模型中使用 `default_scope` 方法指定。
```
class Client < ActiveRecord::Base
default_scope { where("removed_at IS NULL") }
end
```
執行查詢時使用的 SQL 語句如下:
```
SELECT * FROM clients WHERE removed_at IS NULL
```
如果默認作用域中的條件比較復雜,可以使用類方法的形式定義:
```
class Client < ActiveRecord::Base
def self.default_scope
# Should return an ActiveRecord::Relation.
end
end
```
#### 14.4 刪除所有作用域
如果基于某些原因想刪除作用域,可以使用 `unscoped` 方法。如果模型中定義了 `default_scope`,而在這個作用域中不需要使用,就可以使用 `unscoped` 方法。
```
Client.unscoped.load
```
`unscoped` 方法會刪除所有作用域,在數據表中執行常規查詢。
注意,不能在作用域后鏈式調用 `unscoped`,這時可以使用代碼塊形式的 `unscoped` 方法:
```
Client.unscoped {
Client.created_before(Time.zone.now)
}
```
### 15 動態查詢方法
Active Record 為數據表中的每個字段都提供了一個查詢方法。例如,在 `Client` 模型中有個 `first_name` 字段,那么 Active Record 就會生成 `find_by_first_name` 方法。如果在 `Client` 模型中有個 `locked` 字段,就有一個 `find_by_locked` 方法。
在這些動態生成的查詢方法后,可以加上感嘆號(`!`),例如 `Client.find_by_name!("Ryan")`。此時,如果找不到記錄就會拋出 `ActiveRecord::RecordNotFound` 異常。
如果想同時查詢 `first_name` 和 `locked` 字段,可以用 `and` 把兩個字段連接起來,獲得所需的查詢方法,例如 `Client.find_by_first_name_and_locked("Ryan", true)`。
### 16 查找或構建新對象
某些動態查詢方法在 Rails 4.0 中已經啟用,會在 Rails 4.1 中刪除。推薦的做法是使用 Active Record 作用域。廢棄的方法可以在這個 gem 中查看:[https://github.com/rails/activerecord-deprecated_finders](https://github.com/rails/activerecord-deprecated_finders)。
我們經常需要在查詢不到記錄時創建一個新記錄。這種需求可以使用 `find_or_create_by` 或 `find_or_create_by!` 方法實現。
#### 16.1 `find_or_create_by`
`find_or_create_by` 方法首先檢查指定屬性對應的記錄是否存在,如果不存在就調用 `create` 方法。我們來看一個例子。
假設你想查找一個名為“Andy”的客戶,如果這個客戶不存在就新建。這個需求可以使用下面的代碼完成:
```
Client.find_or_create_by(first_name: 'Andy')
# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
```
上述方法生成的 SQL 語句如下:
```
SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT
```
`find_or_create_by` 方法返回現有的記錄或者新建的記錄。在上面的例子中,名為“Andy”的客戶不存在,所以會新建一個記錄,然后將其返回。
新紀錄可能沒有存入數據庫,這取決于是否能通過數據驗證(就像 `create` 方法一樣)。
假設創建新記錄時,要把 `locked` 屬性設為 `false`,但不想在查詢中設置。例如,我們要查詢一個名為“Andy”的客戶,如果這個客戶不存在就新建一個,而且 `locked` 屬性為 `false`。
這種需求有兩種實現方法。第一種,使用 `create_with` 方法:
```
Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')
```
第二種,使用代碼塊:
```
Client.find_or_create_by(first_name: 'Andy') do |c|
c.locked = false
end
```
代碼塊中的代碼只會在創建客戶之后執行。再次運行這段代碼時,會忽略代碼塊中的代碼。
#### 16.2 `find_or_create_by!`
還可使用 `find_or_create_by!` 方法,如果新紀錄不合法,會拋出異常。本文不涉及數據驗證,假設已經在 `Client` 模型中定義了下面的驗證:
```
validates :orders_count, presence: true
```
如果創建新 `Client` 對象時沒有指定 `orders_count` 屬性的值,這個對象就是不合法的,會拋出以下異常:
```
Client.find_or_create_by!(first_name: 'Andy')
# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
```
#### 16.3 `find_or_initialize_by`
`find_or_initialize_by` 方法和 `find_or_create_by` 的作用差不多,但不調用 `create` 方法,而是 `new` 方法。也就是說新建的模型實例在內存中,沒有存入數據庫。繼續使用前面的例子,現在我們要查詢的客戶名為“Nick”:
```
nick = Client.find_or_initialize_by(first_name: 'Nick')
# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
nick.persisted?
# => false
nick.new_record?
# => true
```
因為對象不會存入數據庫,上述代碼生成的 SQL 語句如下:
```
SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1
```
如果想把對象存入數據庫,調用 `save` 方法即可:
```
nick.save
# => true
```
### 17 使用 SQL 語句查詢
如果想使用 SQL 語句查詢數據表中的記錄,可以使用 `find_by_sql` 方法。就算只找到一個記錄,`find_by_sql` 方法也會返回一個由記錄組成的數組。例如,可以運行下面的查詢:
```
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER BY clients.created_at desc")
```
`find_by_sql` 方法提供了一種定制查詢的簡單方式。
#### 17.1 `select_all`
`find_by_sql` 方法有一個近親,名為 `connection#select_all`。和 `find_by_sql` 一樣,`select_all` 方法會使用 SQL 語句查詢數據庫,獲取記錄,但不會初始化對象。`select_all` 返回的結果是一個由 Hash 組成的數組,每個 Hash 表示一個記錄。
```
Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
```
#### 17.2 `pluck`
`pluck` 方法可以在模型對應的數據表中查詢一個或多個字段,其參數是一組字段名,返回結果是由各字段的值組成的數組。
```
Client.where(active: true).pluck(:id)
# SELECT id FROM clients WHERE active = 1
# => [1, 2, 3]
Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']
Client.pluck(:id, :name)
# SELECT clients.id, clients.name FROM clients
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
```
如下的代碼:
```
Client.select(:id).map { |c| c.id }
# or
Client.select(:id).map(&:id)
# or
Client.select(:id, :name).map { |c| [c.id, c.name] }
```
可用 `pluck` 方法實現:
```
Client.pluck(:id)
# or
Client.pluck(:id, :name)
```
和 `select` 方法不一樣,`pluck` 直接把查詢結果轉換成 Ruby 數組,不生成 Active Record 對象,可以提升大型查詢或常用查詢的執行效率。但 `pluck` 方法不會使用重新定義的屬性方法處理查詢結果。例如:
```
class Client < ActiveRecord::Base
def name
"I am #{super}"
end
end
Client.select(:name).map &:name
# => ["I am David", "I am Jeremy", "I am Jose"]
Client.pluck(:name)
# => ["David", "Jeremy", "Jose"]
```
而且,與 `select` 和其他 `Relation` 作用域不同的是,`pluck` 方法會直接執行查詢,因此后面不能和其他作用域鏈在一起,但是可以鏈接到已經執行的作用域之后:
```
Client.pluck(:name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
Client.limit(1).pluck(:name)
# => ["David"]
```
#### 17.3 `ids`
`ids` 方法可以直接獲取數據表的主鍵。
```
Person.ids
# SELECT id FROM people
```
```
class Person < ActiveRecord::Base
self.primary_key = "person_id"
end
Person.ids
# SELECT person_id FROM people
```
### 18 檢查對象是否存在
如果只想檢查對象是否存在,可以使用 `exists?` 方法。這個方法使用的數據庫查詢和 `find` 方法一樣,但不會返回對象或對象集合,而是返回 `true` 或 `false`。
```
Client.exists?(1)
```
`exists?` 方法可以接受多個值,但只要其中一個記錄存在,就會返回 `true`。
```
Client.exists?(id: [1,2,3])
# or
Client.exists?(name: ['John', 'Sergei'])
```
在模型或關系上調用 `exists?` 方法時,可以不指定任何參數。
```
Client.where(first_name: 'Ryan').exists?
```
在上述代碼中,只要有一個客戶的 `first_name` 字段值為 `'Ryan'`,就會返回 `true`,否則返回 `false`。
```
Client.exists?
```
在上述代碼中,如果 `clients` 表是空的,會返回 `false`,否則返回 `true`。
在模型或關系中檢查存在性時還可使用 `any?` 和 `many?` 方法。
```
# via a model
Post.any?
Post.many?
# via a named scope
Post.recent.any?
Post.recent.many?
# via a relation
Post.where(published: true).any?
Post.where(published: true).many?
# via an association
Post.first.categories.any?
Post.first.categories.many?
```
### 19 計算
這里先以 `count` 方法為例,所有的選項都可在后面各方法中使用。
所有計算型方法都可直接在模型上調用:
```
Client.count
# SELECT count(*) AS count_all FROM clients
```
或者在關系上調用:
```
Client.where(first_name: 'Ryan').count
# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
```
執行復雜計算時還可使用各種查詢方法:
```
Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count
```
上述代碼執行的 SQL 語句如下:
```
SELECT count(DISTINCT clients.id) AS count_all FROM clients
LEFT OUTER JOIN orders ON orders.client_id = client.id WHERE
(clients.first_name = 'Ryan' AND orders.status = 'received')
```
#### 19.1 計數
如果想知道模型對應的數據表中有多少條記錄,可以使用 `Client.count` 方法。如果想更精確的計算設定了 `age` 字段的記錄數,可以使用 `Client.count(:age)`。
`count` 方法可用的選項[如前所述](#calculations)。
#### 19.2 平均值
如果想查看某個字段的平均值,可以使用 `average` 方法。用法如下:
```
Client.average("orders_count")
```
這個方法會返回指定字段的平均值,得到的有可能是浮點數,例如 3.14159265。
`average` 方法可用的選項[如前所述](#calculations)。
#### 19.3 最小值
如果想查看某個字段的最小值,可以使用 `minimum` 方法。用法如下:
```
Client.minimum("age")
```
`minimum` 方法可用的選項[如前所述](#calculations)。
#### 19.4 最大值
如果想查看某個字段的最大值,可以使用 `maximum` 方法。用法如下:
```
Client.maximum("age")
```
`maximum` 方法可用的選項[如前所述](#calculations)。
#### 19.5 求和
如果想查看所有記錄中某個字段的總值,可以使用 `sum` 方法。用法如下:
```
Client.sum("orders_count")
```
`sum` 方法可用的選項[如前所述](#calculations)。
### 20 執行 EXPLAIN 命令
可以在關系執行的查詢中執行 EXPLAIN 命令。例如:
```
User.where(id: 1).joins(:posts).explain
```
在 MySQL 中得到的輸出如下:
```
EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
2 rows in set (0.00 sec)
```
Active Record 會按照所用數據庫 shell 的方式輸出結果。所以,相同的查詢在 PostgreSQL 中得到的輸出如下:
```
EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."id" = 1
QUERY PLAN
------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
Join Filter: (posts.user_id = users.id)
-> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
Index Cond: (id = 1)
-> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
Filter: (posts.user_id = 1)
(6 rows)
```
按需加載會觸發多次查詢,而且有些查詢要用到之前查詢的結果。鑒于此,`explain` 方法會真正執行查詢,然后詢問查詢計劃。例如:
```
User.where(id: 1).includes(:posts).explain
```
在 MySQL 中得到的輸出如下:
```
EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
1 row in set (0.00 sec)
EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1)
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
```
#### 20.1 解讀 EXPLAIN 命令的輸出結果
解讀 EXPLAIN 命令的輸出結果不在本文的范疇之內。下面列出的鏈接可以幫助你進一步了解相關知識:
* SQLite3: [EXPLAIN QUERY PLAN](http://www.sqlite.org/eqp.html)
* MySQL: [EXPLAIN 的輸出格式](http://dev.mysql.com/doc/refman/5.6/en/explain-output.html)
* PostgreSQL: [使用 EXPLAIN](http://www.postgresql.org/docs/current/static/using-explain.html)
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes