概要:
本課時講解模型在數(shù)據(jù)查詢時,如何避免 N+1問題,使用 scope 包裝查詢條件,編寫模型 Rspec 測試。
知識點:
N+1
Scope
實用的查詢
正文
4.2.1 兩個 Gem
ActiveRecord 這個 gem 中,包含了兩個重要的 gem,打開它的 源代碼,可以看到這兩個 gem:activemodel 和 arel。
activemodel
為一個類增加了許多特性,比如屬性校驗,回調(diào)等,這在后面章節(jié)會介紹。
arel
是 Ruby 編寫的 sql 工具,使用它,可以通過簡單的 Ruby 語法,編寫復雜 sql 查詢,我們上面使用的例子,語法就來自 arel。arel 還可以面向多種關系型數(shù)據(jù)庫。
ActiveRecord 在使用 arel 的時候,提供了一個方法:sanitize_sql。
在我們以上的講解中,會經(jīng)常傳遞這樣的參數(shù)["name = ? and price=?", "foobar", 4]
,它會由sanitize_sql
方法進行處理,這是一個 protected 方法,我們使用 send 來調(diào)用它:
Product.send(:sanitize_sql, ["name = ? and price=?", "Shoes", 4])=> "name = 'Shoes' and price=4"
這是一種安全的手段,保護我們的 sql 不會被插入惡意代碼。我們不必去直接使用這個方法,除非特殊情況,我們只需要按照它的格式要求來書寫就可以了。
4.2.2 N+1
N+1 是查詢中經(jīng)常遇到的一個問題。在下一節(jié)里,我們經(jīng)常使用關聯(lián)關系的查詢,比如,列出十個用戶的同時,顯示它地址中的電話:
users = User.limit(10)users.each do |user| puts user.address.phoneend
這樣就會造成,在 each 中又去查詢數(shù)據(jù),得到電話。這種情況會經(jīng)常出現(xiàn)在我的列表中,所以在列表中會經(jīng)常遇到 N+1 的問題。
為了避免這個問題,Rails 提供了預加載的功能,在查詢的時候,使用includes
來解決。上面的例子修改一下:
users = User.includes(:address).limit(10)users.each do |user| puts user.address.phoneend
我們查看一下終端的輸出:
SELECT * FROM users LIMIT 10SELECT addresses.* FROM addresses WHERE (addresses.user_id IN (1,2,3,4,5,6,7,8,9,10))
這里只有兩個 sql 查詢,提高了查詢效率。
4.2.3 查詢中使用 Scope
當我們使用 where 查詢的時候,會遇到多個條件組合查詢。通常我們可以把它們都寫到一個 where 的條件里,比如:
Product.where(name: "T-Shirt", hot: true, top: true)
我增加了兩個條件,hot: true
和top: true
,但是,這種條件組合只能在這里使用,在其他地方,我們還要再寫一遍,這不符合 Rails 的哲學:“不要重復自己”。
Rails 提供了 scope,讓我們復用查詢條件:
class Product < ActiveRecord::Base scope :hot, -> { where(hot: true) } scope :top, -> { where(top: true) }end
使用的時候,我們可以將多個 scope 組合在一起:
Product.top.hot.where(name: "T-Shirt")
default_scope
可以為所有查詢加上它定義的查詢條件,比如:
class Product < ActiveRecord::Base default_scope { where("deleted_at IS NULL") }end
default_scope
要慎用,慎用,慎用(重要的話說三遍),在我們程序變的復雜的時候,性能往往會消耗在數(shù)據(jù)庫查詢上,維護已有查詢時,很容易忽視 default_scope 的作用。如果使用了 default_scope,而在其他地方不得不去掉它,可以使用 unscoped,然后再附上其他查詢:
Product.unscoped.load.top.hot
如果一個地方使用了某個 scope,而要在另一個地方把它的條件改變,可以使用 merge:
class Product < ActiveRecord::Base scope :active, -> { where state: 'active' } scope :inactive, -> { where state: 'inactive' }end
看一下它的執(zhí)行結果:
Product.active.merge(User.inactive)# SELECT "products".* FROM "products" WHERE "products"."state" = 'inactive'
4.2.4 實用的查詢
4.2.4.1 sql 查詢集合
我們使用where查詢,得到的是 ActiveRecord::Relation 實例,它的源代碼在這里。閱讀這里的代碼,會讓你學習到更多優(yōu)雅的查詢方法。在查詢時,我們還可以使用 sql 直接查詢,如果你更熟悉 sql 語法,可以這樣來查詢:
Client.find_by_sql("SELECT * FROM clients INNER JOIN orders ON clients.id = orders.client_id ORDER BY clients.created_at desc")# => [ #<Client id: 1, first_name: "Lucas" >, #<Client id: 2, first_name: "Jan" >, # ...]
這個例子來自這里。
它返回的是實例的集合,這在我們 Rails 內(nèi)使用很方便,但是提供 json 格式的 api時,需要轉換一下,不過我們可以用 select_all 查詢,得到包含 hash 的 array:
Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")# => [ {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}]
4.2.4.2 pluck
pluck 可以直接在 Relation 實例的基礎上,使用 sql 的 select 方法,得到字段值的集合(Array),而不用把返回結果包裝成 ActiveRecord 實例,再得到屬性值。在查詢屬性集合時,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']]
ActiveRecord 有一個類似的方法,select,比較下兩者的區(qū)別:
Product.select(:id, :name) Product Load (8.5ms) SELECT "products"."id", "products"."name" FROM "products" => #<ActiveRecord::Relation [#<Product id: 1, name: "f">]> Product.pluck(:id, :name) (0.3ms) SELECT "products"."id", "products"."name" FROM "products" => [[1, "f"]]
前者顯示返回 AR 實例,然后取其屬性值,后者直接讀取數(shù)據(jù)庫記錄,返回數(shù)組。
pluck 只能用在查詢的最后,因為它直接返回了結果,而不是 ActiveRecord::Relation。
4.2.4.3 ids
ids 返回主鍵集合:
Person.ids=> SELECT id FROM people
不要被 ids 字面迷惑,它返回的是主鍵的集合,我們可以在 model 里設定其他字段為主鍵。
class Person < ActiveRecord::Base self.primary_key = "person_id"endPerson.ids=> SELECT person_id FROM people
4.2.4.4 查詢記錄數(shù)量
這里有四個方法,方便我們判斷一個模型中的記錄數(shù)量。
Client.exists?(1)Client.exists?(id: [1,2,3])Client.exists?(name: ['John', 'Sergei'])
exists?
判斷記錄是否存在,和它類似的方法有兩個:
Client.exists? [1]Client.any? [2]Client.many? [3]
[1] 是否有記錄[2] 是否至少有一條記錄[3] 是否有多于一條的記錄
any? 和 many? 與 exists? 不同的是,他們可以使用在 Relation 實例上,比如:
Article.where(published: true).any?Article.where(published: true).many?
還可以接收 block:
person.pets.any? do |pet| pet.group == 'cats'end=> falseperson.pets.many? do |pet| pet.group == 'dogs'end=> true
4.2.4.5 查詢記錄數(shù)量
下面五個方法,完全可以按照字面意義理解,并且適用于 Relation 上:
Client.countClient.average("orders_count")Client.minimum("age")Client.maximum("age")Client.sum("orders_count")
以上的例子來自 這里,閑暇的時候應該多讀讀這個文檔,翻看源碼。