[Rails] Active Record Queries

資料來(lái)源:Rails Guide

Guide

-“查詢”就是根據(jù)條件查找記錄。
-“查詢操作”多種多樣:select, where, order, etc
-“方法鏈”:將一系列查詢操作的方法通過(guò).串鏈在一起
-“預(yù)載入”eager load通過(guò)加載關(guān)聯(lián)對(duì)象,以減少查詢次數(shù)
-如何檢查查詢的記錄是否存在,或關(guān)聯(lián)的對(duì)象是否存在

1. Why

為什么要使用關(guān)聯(lián)?
-(1) 可增強(qiáng)代碼可讀性
-(2) 不依賴于特定的數(shù)據(jù)庫(kù)
-(3) 嘿嘿最爽的,不用寫(xiě)復(fù)雜的 SQL
Tips: 某些特殊情況為了提高效率你還是得寫(xiě) :(

2. What

查詢 通過(guò)指定的條件得到你想要的結(jié)果

2.1 查詢步驟

-(1) 將查詢方法轉(zhuǎn)化為 SQL 查詢語(yǔ)句
-(2) 執(zhí)行 SQL 查詢語(yǔ)句,返回?cái)?shù)據(jù)庫(kù)中相關(guān)記錄
-(3) 將相關(guān)記錄轉(zhuǎn)化為關(guān)聯(lián)模型的 Ruby 對(duì)象
-(4) 執(zhí)行after_findafter_initialize 回調(diào)

2.2 查詢結(jié)果通常可分為兩類

-(1) 若查找一條記錄,返回的是模型對(duì)象的一個(gè)實(shí)例;
-(2) 若查找結(jié)果為多個(gè)模型對(duì)象實(shí)例的集合,返回ActiveRecord::Relation對(duì)象
Tip: 區(qū)分··關(guān)聯(lián)查找返回的是ActiveRecord::Association::CollectionProxy對(duì)象
Tip: 若是通過(guò)關(guān)聯(lián)在進(jìn)行查詢,返回ActiveRecord::AssociationRelation對(duì)象

2.2.1 返回單個(gè)對(duì)象

查詢方法:find, find_by, take, first, last
Tip1: find找不到時(shí)拋出ActiveRecord::RecordNotFound,其他方法返回nil
Tip2: 若想讓其他方法也拋出ActiveRecord::RecordNotFound,則使用爆破方法
Tip3: 這些方法也可以返回多條記錄(數(shù)組),find后面跟一組id組成的數(shù)組可返回多個(gè)記錄,其他方法加數(shù)量參數(shù)也可返回多個(gè)記錄;注意find_by方法無(wú)法返回?cái)?shù)組,屬性對(duì)應(yīng)數(shù)組的話只是增加查詢所要滿足的條件

2.2.2 返回多個(gè)對(duì)象(不含條件)

-(1) all載入所有記錄到內(nèi)存中,當(dāng)數(shù)據(jù)量很大時(shí)會(huì)爆炸!
-(2) find_each將記錄分成大小相等的塊,一塊一塊地載入,代碼塊中處理單個(gè)記錄
-(3) find_in_batch同上,分塊載入,代碼塊中處理整個(gè)載入的塊 (數(shù)組)
-分塊載入方法添加選項(xiàng) batch_size, start, finish 分別指定大小,開(kāi)始結(jié)束位置

Tip: the 'find_each' and 'find_in_batch' methods are intended for use in the batch processing of large number of records that wouldn't fit in memory all at once. if you just need to loop over a thousand records the regular find methods (all, where...) are the preferred option.

2.2.3 返回多個(gè)對(duì)象(含條件)

2.2.3.1 where

條件可以是純字符串 (存在SQL注入),數(shù)組,占位符,哈希鍵值對(duì)
一般情況下建議使用哈希的形式 (Rails way),若不能滿足使用數(shù)組或占位符
哈希鍵值對(duì)的使用可分為三種情況: (1) Equality (2) Range (3) Subset
Tip: not跟在where后面是用表示不等于。

2.2.3.2 order

order對(duì)查找的結(jié)果排序 (:asc 升序, :desc 降序),默認(rèn)為升序

2.2.3.3 select

默認(rèn)情況下,會(huì)查詢記錄中所有字段,使用select可以指定要查找的字段
distinct query = Client.select(:id, :name).distinct <~> query.distinct(false)
Tip1: 后面接distinct方法查找單個(gè)記錄,也可使用distinct(false)去除限制
Tip2: 當(dāng)要查找的字段不存在時(shí)會(huì)拋出ActiveRecord::MissingAttributeError
Tip3: 若指定的字段不含id則關(guān)聯(lián)將會(huì)失效
Tip4: 只查找個(gè)別字段會(huì)提高查詢的效率

2.2.3.4 limit & offset

limit 來(lái)指定你想得到多少條記錄
offset 來(lái)制定你想跳過(guò)多少條記錄后開(kāi)始查找
query = Client.limit(5).offset(30) # start with 31st get 5 client records

2.2.3.5 group & having

group 用來(lái)把屬性相同的記錄組織在一起,用于計(jì)算等
having 用來(lái)對(duì)使用group之后計(jì)算得到的值來(lái)限制條件

.group("date(created_at)").having("sum(price) > ?", 100)```
**Tip**: ```group```方法后面加```count```可以的到每組對(duì)應(yīng)的記錄個(gè)數(shù) (Hash)
```Order.group(:status).count # => { 'awaiting_approval' => 7, 'paid' => 12 }```

#### 2.2.3.6 Overriding Conditions
```unscope``` 移除之前定義過(guò)的某個(gè)查詢方法或某個(gè)條件
```Article.where('id > 10').limit(20).order('id asc').unscope(:order)```
```Article.where(id: 10, trashed: false).unscope(where: :id)```
```only``` 用來(lái)限制指定的查詢方法是有效的,其他無(wú)效
```reorder``` 用來(lái)重新指定排序的字段
```reverse_order``` 與之前的排序相反
```rewhere``` 重新指定查詢的條件

#### 2.2.3.7 none
只要查詢方法鏈中包含```none```就返回空的查詢結(jié)果 ```#<ActiveRecord::Relation []>```
```User.none if user.in_blacklist? # => #<ActiveRecord::Relation []> ```
**Tip**: 當(dāng)你在特定情況下,如該用戶被拉入黑名單,他的查詢操作結(jié)果應(yīng)該為空

#### 2.2.3.8 readonly
一個(gè)查詢結(jié)果后添加```readonly```用來(lái)明確指出該記錄是不可被修改的
若你強(qiáng)行修改它系統(tǒng)會(huì)拋出```ActiveRecord::ReadOnlyRecord```.

#### 2.2.3.9 joins
```joins``` 后面可直接加 SQL 語(yǔ)句來(lái)進(jìn)行查詢,或接關(guān)聯(lián)
```joins``` 還可以連接多個(gè)關(guān)聯(lián),可以連接嵌套關(guān)聯(lián)
```left_outer_joins``` 用來(lái)處理左連接,即使關(guān)聯(lián)為空,也會(huì)放入查詢結(jié)果
```ruby
Author.left_outer_joins(:posts).distinct
      .select('authors.*, COUNT(posts.*) AS posts_count')
      .group('authors.id')

=> (sql) SELECT DISTINCT authors.*, COUNT(posts.*) AS posts_count FROM "authors"
         LEFT OUTER JOIN posts ON posts.author_id = authors.id GROUP BY authors.id

3. Existence of Objects

比較這些方法:present?, exists?, any?, many?
-(1) 避免使用 present? 查看結(jié)果是否存在,效率較低:
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 1]]
-(2) exists?, any?查看結(jié)果是否存在,效率較高 (查到一條記錄存在就停止)
SELECT 1 AS one FROM "posts" WHERE "posts"."user_id"=? LIMIT ? [..., limit 1]
-(3) 使用 many? 查看結(jié)果是否有不止一個(gè) (查詢語(yǔ)句使用count):
SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 1]]]
Tip: 和any?相比,exists?后面可以加條件參數(shù),查看滿足條件的記錄是否存在
Client.exists?(name: ['Wende', 'Zhaobo', 'Junda'])
Client.where(name: ['Wende', 'Zhaobo', 'Junda']).exists?

4. Locking

給記錄上鎖是為了避免多人同時(shí)操作同一條記錄可能產(chǎn)生的沖突。

4.1 Optimistic Locking

  • 樂(lè)觀鎖允許多個(gè)用戶獲取并操作同一記錄,在操作完成并提交更新時(shí)通過(guò)查看該記錄的版本來(lái)判斷是否會(huì)發(fā)生沖突,若版本不一致拋出ActiveRecord::StaleObjectError并默認(rèn)無(wú)視所更新的內(nèi)容。對(duì)于失敗的情況我們要處理這種異常,回滾,合并或是進(jìn)行其他邏輯操作來(lái)彌補(bǔ)。(實(shí)現(xiàn)方式通過(guò)添加lock_version自動(dòng)實(shí)現(xiàn))
  • 樂(lè)觀鎖用來(lái)處理競(jìng)爭(zhēng)關(guān)系,誰(shuí)都可以獲取記錄的操作權(quán),誰(shuí)先完成,誰(shuí)就可以更新記錄,其他的更新將會(huì)在本次競(jìng)爭(zhēng)中失敗。所以少了等待的過(guò)程。

4.2 Pessimistic Locking

  • 悲觀鎖是當(dāng)某個(gè)用戶準(zhǔn)備操作某條記錄之前,給這個(gè)記錄或表上鎖,這樣其他人在他沒(méi)完成操作之前都無(wú)法打開(kāi)這個(gè)鎖,不存在競(jìng)爭(zhēng)關(guān)系,必定是這個(gè)上鎖的人完成更新操作。(悲觀鎖是數(shù)據(jù)庫(kù)底層自帶的鎖機(jī)制,使用lock, lock!, with_lock來(lái)上鎖)
  • 悲觀鎖可確保第一個(gè)獲取記錄操作權(quán)的人完成更新。并且避免大量用戶操作同個(gè)記錄中只有一人更新其他則做無(wú)用功的情況,在操作完成時(shí)更新比較記錄鎖的版本號(hào)也是要耗費(fèi)性能的。

5. Eager Loading Association

通過(guò)使用includes方法解決 n +1 queries 問(wèn)題
Category.includes(articles: [ { comments: :guest}, :tags ]).find(1)
上面的例子會(huì)找到 id 為 1 的類別記錄,并且加載他所有關(guān)聯(lián)的文章,文章相關(guān)的標(biāo)簽和評(píng)論,還有相關(guān)評(píng)論人,這樣你在對(duì)這些數(shù)據(jù)進(jìn)行操作時(shí)就不必在進(jìn)行其他查詢了

6. Scopes

Scope 可以讓你把常用的關(guān)聯(lián)查詢語(yǔ)句構(gòu)建成為一個(gè)類方法,供類和關(guān)聯(lián)對(duì)象使用
它的返回值統(tǒng)一為relation這樣便于調(diào)用鏈方法,就好像是自己構(gòu)建的一個(gè)查詢方法
Tips: scope 和類方法的區(qū)別?scope 返回一個(gè)ActiveRecord::Relation對(duì)象,即使當(dāng)查詢條件不滿足時(shí)也會(huì)返回一個(gè)空的relation。反觀類方法當(dāng)結(jié)果不滿足時(shí)你可以返回任何你想返回的值,如nil,而這就會(huì)導(dǎo)致查詢鏈斷裂,因?yàn)闊o(wú)法對(duì)一個(gè)空的對(duì)象在進(jìn)行更多查詢了。

7. Enums

enum availability: [:available, :unavailable]
使用enum宏方法來(lái)匹配類型為integer的字段,已達(dá)到枚舉不同狀態(tài)的效果。
使用它非常方便清晰,會(huì)自動(dòng)擁有許多幫助方法,當(dāng)情況更加復(fù)雜時(shí)可以使用狀態(tài)機(jī)。

8. Dynamic Finders

find_by_first_name('wende'), find_by_last_name('lu'), find_by_age(25)
find_by_first_name_and_last_name_and_age('wende', 'lu', 25)

9. Method Chain

在方法鏈末尾使用where來(lái)過(guò)濾得到一組記錄。
在方法鏈末尾使用find_by來(lái)過(guò)濾得到一條記錄。

10. Find or Build a new Object

find_or_create_by, find_or_initialize_by 創(chuàng)建或初始化一條記錄如果它不存在。
Tip1: 使用created_with或代碼塊在創(chuàng)建時(shí)賦值,注意在查詢時(shí)該語(yǔ)句將會(huì)被忽略。
Tip2: 使用persisted?, new_record?來(lái)判斷記錄是否存在在數(shù)據(jù)庫(kù)中。

11. Find by SQL

find_by_sql 使用自定義的 SQL 將查詢結(jié)果初始化為對(duì)象存到數(shù)組中。
connection#select_all 自定義查詢,獲取由哈希鍵值對(duì)所組成的數(shù)組 (不初始化對(duì)象)。
pluck 接收一系列列名作為參數(shù)并且返回包含記錄中該列的值的數(shù)組。
Client.pluck(:id) 等價(jià)于 Client.select(:id).map(&:id)
Client.pluck(:id, :name) 等價(jià)于 Client.select(:id, :name).map {|c| [c.id, c.name]}
Tips: 在一個(gè)已查詢的relation上使用pluck并不會(huì)重新去查詢數(shù)據(jù)庫(kù)。
Tips: ids 相當(dāng)于使用pluck獲得所有主鍵id的數(shù)組。
Tips: select_all, pluck 直接把數(shù)據(jù)庫(kù)查詢的結(jié)果轉(zhuǎn)化為數(shù)組,并不會(huì)創(chuàng)建模型對(duì)象。對(duì)于操作量打并且查詢次數(shù)頻繁的查詢操作,可以提高效率。

12. Calculations

count, average, minimum, maximum, sum
Tips: count后可以添加列名,表示查找該列名存在的記錄個(gè)數(shù)。
Tips: 這些計(jì)算操作都是在數(shù)據(jù)庫(kù)層執(zhí)行 SQL 計(jì)算得到的,需要對(duì)數(shù)據(jù)庫(kù)操作。
為提高效率,我么希望先獲得查詢結(jié)果,轉(zhuǎn)化為普通數(shù)組在進(jìn)行操作,以減少查詢。

13. Explain

對(duì)relation使用explain方法可以查看該查詢語(yǔ)句的詳細(xì)信息,用于調(diào)優(yōu)。
不同數(shù)據(jù)庫(kù)都有自己的EXPLAIN方法,查看并掌握他們十分有用。
Tips: 對(duì)relation使用to_sql可以查看該查詢方法鏈所生成的 SQL 語(yǔ)句。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容