在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Ruby/ 4.2 深入模型查詢
寫在后面
寫在前面
第六章 Rails 的配置及部署
第四章 Rails 中的模型
4.4 模型中的校驗(Validates)
1.3 用戶界面(UI)設(shè)計
6.5 生產(chǎn)環(huán)境部署
3.2 表單
4.3 模型中的關(guān)聯(lián)關(guān)系(Relations)
4.5 模型中的回調(diào)(Callback)
第五章 Rails 中的控制器
4.2 深入模型查詢
5.2 控制器中的方法
6.2 緩存
3.4 模板引擎的使用
6.4 I18n
第一章 Ruby on Rails 概述
6.6 常用 Gem
1.2 Rails 文件簡介
2.2 REST 架構(gòu)
2.3 深入路由(routes)
第三章 Rails 中的視圖
6.3 異步任務(wù)及郵件發(fā)送
第二章 Rails 中的資源
3.3 視圖中的 AJAX 交互

4.2 深入模型查詢

概要:

本課時講解模型在數(shù)據(jù)查詢時,如何避免 N+1問題,使用 scope 包裝查詢條件,編寫模型 Rspec 測試。

知識點:

  1. N+1
  2. Scope
  3. 實用的查詢
  4. Rspec 測試

正文

4.2.1 兩個 Gem

ActiveRecord 這個 gem 中,包含了兩個重要的 gem,打開它的 源代碼,可以看到這兩個 gem:activemodelarel。

activemodel 為一個類增加了許多特性,比如屬性校驗,回調(diào)等,這在后面章節(jié)會介紹。

arel 是 Ruby 編寫的 sql 工具,使用它,可以通過簡單的 Ruby 語法,編寫復(fù)雜 sql 查詢,我們上面使用的例子,語法就來自 arel。arel 還可以面向多種關(guān)系型數(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)常使用關(guān)聯(lián)關(guān)系的查詢,比如,列出十個用戶的同時,顯示它地址中的電話:

users = User.limit(10)

users.each do |user|
  puts user.address.phone
end

這樣就會造成,在 each 中又去查詢數(shù)據(jù),得到電話。這種情況會經(jīng)常出現(xiàn)在我的列表中,所以在列表中會經(jīng)常遇到 N+1 的問題。

為了避免這個問題,Rails 提供了預(yù)加載的功能,在查詢的時候,使用 includes 來解決。上面的例子修改一下:

users = User.includes(:address).limit(10)

users.each do |user|
  puts user.address.phone
end

我們查看一下終端的輸出:

SELECT * FROM users LIMIT 10
SELECT 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: truetop: true,但是,這種條件組合只能在這里使用,在其他地方,我們還要再寫一遍,這不符合 Rails 的哲學(xué):“不要重復(fù)自己”。

Rails 提供了 scope,讓我們復(fù)用查詢條件:

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 要慎用,慎用,慎用(重要的話說三遍),在我們程序變的復(fù)雜的時候,性能往往會消耗在數(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í)行結(jié)果:

Product.active.merge(User.inactive)
# SELECT "products".* FROM "products" WHERE "products"."state" = 'inactive'

4.2.4 實用的查詢

4.2.4.1 sql 查詢集合

我們使用where查詢,得到的是 ActiveRecord::Relation 實例,它的源代碼在這里。閱讀這里的代碼,會讓你學(xué)習(xí)到更多優(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時,需要轉(zhuǎn)換一下,不過我們可以用 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 實例的基礎(chǔ)上,使用 sql 的 select 方法,得到字段值的集合(Array),而不用把返回結(jié)果包裝成 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 只能用在查詢的最后,因為它直接返回了結(jié)果,而不是 ActiveRecord::Relation。

4.2.4.3 ids

ids 返回主鍵集合:

Person.ids
=> SELECT id FROM people

不要被 ids 字面迷惑,它返回的是主鍵的集合,我們可以在 model 里設(shè)定其他字段為主鍵。

class Person < ActiveRecord::Base
  self.primary_key = "person_id"
end

Person.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
=> false

person.pets.many? do |pet|
  pet.group == 'dogs'
end
=> true

4.2.4.5 查詢記錄數(shù)量

下面五個方法,完全可以按照字面意義理解,并且適用于 Relation 上:

Client.count
Client.average("orders_count")
Client.minimum("age")
Client.maximum("age")
Client.sum("orders_count")

以上的例子來自 這里,閑暇的時候應(yīng)該多讀讀這個文檔,翻看源碼。

4.2.5 Rspec 測試

在深入 Rails 項目開發(fā)之后,測試環(huán)節(jié)是一個重要的環(huán)節(jié)。Ruby 為我們提供了非常方便的測試框架,Rails 也可以方便的執(zhí)行這些測試框架。

在 Rails 3.x 及之前的版本里,默認使用 TestUnit 框架,4.x 之后改為 MiniTest 框架。我們可以查看 test_case.rb 文件,看到其中的變化。

除了這兩個測試框架,Rspec 也是經(jīng)常用到的 Ruby 測試框架。

我們在 Rails 里安裝 rpesc,和其他的幾個 gem:

group :development, :test do
  gem 'rspec-rails'
  gem "factory_girl_rails"
  gem "database_cleaner"
end

rspec-railsrspec 的 Rails 集成,在 Rails 中初始化 rspec 的命令是:

rails generate rspec:install

它會創(chuàng)建兩個文件,和 spec 文件件。運行 rpsec 測試的命令非常簡單,rspec 就可以,他會自動運行 spec 文件夾下所有的 xxx_spec.rb 文件,也可以指定某個文件:

rspec spec/models/product_spec.rb

也可以只運行某一個測試用例,這需要指定該用例開始的行數(shù):

rspec spec/models/product_spec.rb:10

也可以運行某一個目錄:

rspec spec/models/

factory_girl_railsfactory_girl 的 Rails 包裝。factory_girl 可以為我們的測試代碼提供模擬的測試數(shù)據(jù)。

database_cleaner 可以在每一次運行測試的時候,清空測試數(shù)據(jù)庫。我們在 config/database.yml 中,會設(shè)置三種運行環(huán)境,test 環(huán)境要單獨設(shè)置數(shù)據(jù)庫,也就是因為測試時會反復(fù)填入和刪除數(shù)據(jù)。一般,test 使用的是 sqlite 數(shù)據(jù)庫,而 production 使用 mysql、postgresql 等數(shù)據(jù)庫。

我們需要配置下 spec 的運行環(huán)境:

RSpec.configure do |config|
  config.before(:each) do
    DatabaseCleaner.strategy = :truncation
    DatabaseCleaner.clean
  end
end

4.2.5.1 Model 測試

在使用 generator 創(chuàng)建 model 文件的時候,rspec 會自動創(chuàng)建它對應(yīng)的 spec 文件。我們打開 product_spec.rb 文件:

require 'rails_helper'

RSpec.describe Product, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end

我們?yōu)樗黾右粋€測試:

RSpec.describe Product, type: :model do
  it "should create a product" do
    tshirt = Product.create(name: "T-Shirt", price: 9.99)

    expect(tshirt.name).to eq("T-Shirt")
    expect(tshirt.price).to eq(9.99)
  end
end

運行一下這個測試:

rspec spec/models/product_spec.rb
.

Finished in 0.081 seconds (files took 2.37 seconds to load)
1 example, 0 failures

這個測試的目的,是確保 create 方法可以為我們創(chuàng)建一個 product 實例。更多 rspec 語法可以查看 rspec 文檔,或者 《使用 RSpec 測試 Rails 程序》一書。