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

鍍金池/ 教程/ Ruby/ 4.2 深入模型查詢(xún)
寫(xiě)在后面
寫(xiě)在前面
第六章 Rails 的配置及部署
第四章 Rails 中的模型
4.4 模型中的校驗(yàn)(Validates)
1.3 用戶(hù)界面(UI)設(shè)計(jì)
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 深入模型查詢(xún)
5.2 控制器中的方法
6.2 緩存
3.4 模板引擎的使用
6.4 I18n
第一章 Ruby on Rails 概述
6.6 常用 Gem
1.2 Rails 文件簡(jiǎn)介
2.2 REST 架構(gòu)
2.3 深入路由(routes)
第三章 Rails 中的視圖
6.3 異步任務(wù)及郵件發(fā)送
第二章 Rails 中的資源
3.3 視圖中的 AJAX 交互

4.2 深入模型查詢(xún)

概要:

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

知識(shí)點(diǎn):

  1. N+1
  2. Scope
  3. 實(shí)用的查詢(xún)
  4. Rspec 測(cè)試

正文

4.2.1 兩個(gè) Gem

ActiveRecord 這個(gè) gem 中,包含了兩個(gè)重要的 gem,打開(kāi)它的 源代碼,可以看到這兩個(gè) gem:activemodelarel。

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

arel 是 Ruby 編寫(xiě)的 sql 工具,使用它,可以通過(guò)簡(jiǎn)單的 Ruby 語(yǔ)法,編寫(xiě)復(fù)雜 sql 查詢(xún),我們上面使用的例子,語(yǔ)法就來(lái)自 arel。arel 還可以面向多種關(guān)系型數(shù)據(jù)庫(kù)。

ActiveRecord 在使用 arel 的時(shí)候,提供了一個(gè)方法:sanitize_sql。

在我們以上的講解中,會(huì)經(jīng)常傳遞這樣的參數(shù) ["name = ? and price=?", "foobar", 4],它會(huì)由 sanitize_sql 方法進(jìn)行處理,這是一個(gè) protected 方法,我們使用 send 來(lái)調(diào)用它:

Product.send(:sanitize_sql, ["name = ? and price=?", "Shoes", 4])
=> "name = 'Shoes' and price=4"

這是一種安全的手段,保護(hù)我們的 sql 不會(huì)被插入惡意代碼。我們不必去直接使用這個(gè)方法,除非特殊情況,我們只需要按照它的格式要求來(lái)書(shū)寫(xiě)就可以了。

4.2.2 N+1

N+1 是查詢(xún)中經(jīng)常遇到的一個(gè)問(wèn)題。在下一節(jié)里,我們經(jīng)常使用關(guān)聯(lián)關(guān)系的查詢(xún),比如,列出十個(gè)用戶(hù)的同時(shí),顯示它地址中的電話(huà):

users = User.limit(10)

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

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

為了避免這個(gè)問(wèn)題,Rails 提供了預(yù)加載的功能,在查詢(xún)的時(shí)候,使用 includes 來(lái)解決。上面的例子修改一下:

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))

這里只有兩個(gè) sql 查詢(xún),提高了查詢(xún)效率。

4.2.3 查詢(xún)中使用 Scope

當(dāng)我們使用 where 查詢(xún)的時(shí)候,會(huì)遇到多個(gè)條件組合查詢(xún)。通常我們可以把它們都寫(xiě)到一個(gè) where 的條件里,比如:

Product.where(name: "T-Shirt", hot: true, top: true)

我增加了兩個(gè)條件,hot: truetop: true,但是,這種條件組合只能在這里使用,在其他地方,我們還要再寫(xiě)一遍,這不符合 Rails 的哲學(xué):“不要重復(fù)自己”。

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

class Product < ActiveRecord::Base
  scope :hot, -> { where(hot: true) }
  scope :top, -> { where(top: true) }
end

使用的時(shí)候,我們可以將多個(gè) scope 組合在一起:

Product.top.hot.where(name: "T-Shirt")

default_scope 可以為所有查詢(xún)加上它定義的查詢(xún)條件,比如:

class Product < ActiveRecord::Base
  default_scope { where("deleted_at IS NULL") }
end

default_scope 要慎用,慎用,慎用(重要的話(huà)說(shuō)三遍),在我們程序變的復(fù)雜的時(shí)候,性能往往會(huì)消耗在數(shù)據(jù)庫(kù)查詢(xún)上,維護(hù)已有查詢(xún)時(shí),很容易忽視 default_scope 的作用。如果使用了 default_scope,而在其他地方不得不去掉它,可以使用 unscoped,然后再附上其他查詢(xún):

Product.unscoped.load.top.hot

如果一個(gè)地方使用了某個(gè) scope,而要在另一個(gè)地方把它的條件改變,可以使用 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 實(shí)用的查詢(xún)

4.2.4.1 sql 查詢(xún)集合

我們使用where查詢(xún),得到的是 ActiveRecord::Relation 實(shí)例,它的源代碼在這里。閱讀這里的代碼,會(huì)讓你學(xué)習(xí)到更多優(yōu)雅的查詢(xún)方法。在查詢(xún)時(shí),我們還可以使用 sql 直接查詢(xún),如果你更熟悉 sql 語(yǔ)法,可以這樣來(lái)查詢(xún):

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" >,
  # ...
]

這個(gè)例子來(lái)自這里。

它返回的是實(shí)例的集合,這在我們 Rails 內(nèi)使用很方便,但是提供 json 格式的 api時(shí),需要轉(zhuǎn)換一下,不過(guò)我們可以用 select_all 查詢(xún),得到包含 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 實(shí)例的基礎(chǔ)上,使用 sql 的 select 方法,得到字段值的集合(Array),而不用把返回結(jié)果包裝成 ActiveRecord 實(shí)例,再得到屬性值。在查詢(xún)屬性集合時(shí),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 有一個(gè)類(lèi)似的方法,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í)例,然后取其屬性值,后者直接讀取數(shù)據(jù)庫(kù)記錄,返回?cái)?shù)組。

pluck 只能用在查詢(xún)的最后,因?yàn)樗苯臃祷亓私Y(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 查詢(xún)記錄數(shù)量

這里有四個(gè)方法,方便我們判斷一個(gè)模型中的記錄數(shù)量。

Client.exists?(1)
Client.exists?(id: [1,2,3])
Client.exists?(name: ['John', 'Sergei'])

exists? 判斷記錄是否存在,和它類(lèi)似的方法有兩個(gè):

Client.exists? [1]
Client.any? [2]
Client.many? [3]

[1] 是否有記錄 [2] 是否至少有一條記錄 [3] 是否有多于一條的記錄

any? 和 many? 與 exists? 不同的是,他們可以使用在 Relation 實(shí)例上,比如:

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 查詢(xún)記錄數(shù)量

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

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

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

4.2.5 Rspec 測(cè)試

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

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

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

我們?cè)?Rails 里安裝 rpesc,和其他的幾個(gè) 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

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

rspec spec/models/product_spec.rb

也可以只運(yùn)行某一個(gè)測(cè)試用例,這需要指定該用例開(kāi)始的行數(shù):

rspec spec/models/product_spec.rb:10

也可以運(yùn)行某一個(gè)目錄:

rspec spec/models/

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

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

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

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

4.2.5.1 Model 測(cè)試

在使用 generator 創(chuàng)建 model 文件的時(shí)候,rspec 會(huì)自動(dòng)創(chuàng)建它對(duì)應(yīng)的 spec 文件。我們打開(kāi) product_spec.rb 文件:

require 'rails_helper'

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

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

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

運(yùn)行一下這個(gè)測(cè)試:

rspec spec/models/product_spec.rb
.

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

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