接續 Rails 打基礎 - Rails Guides 3.2.13 閱讀筆記 Part 1

Active Record Query Interface

find_each 與 find_in_batches

來自 1.3 Retrieving Multiple Objects in Batches

find_each 用起來跟 each 差不多,但預設一次只會從 DB 撈 1,000 筆,尤其當資料多起來以後,直接下 User.all 會很吃記憶體。

User.find_each do |user|
  NewsLetter.weekly_deliver(user)
end

find_in_batches 也是預設一次 1,000 筆,但讀出來的變數是 array。(追 source code 可以發現,find_each 實際上是去呼叫 find_in_batches

Invoice.find_in_batches(:include => :invoice_lines) do |invoices|
  export.add_invoices(invoices)
end

兩者都有 :batch_size:start 參數可以設定。

但他有些限制,例如不能指定排序。根據 API 文件

It’s not possible to set the order. That is automatically set to ascending on the primary key (“id ASC”) to make the batch ordering work. This also mean that this method only works with integer-based primary keys. …

Range Conditions 的精美語法

來自 2.2.2 Range Conditions

下範圍條件,我原本的寫法

Client.where("created_at BETWEEN ? AND ?", params[:start_date].to_date, params[:end_date].to_date)

可以改成精美的語法

Client.where(:created_at => (params[:start_date].to_date)..(params[:end_date].to_date))

reorder

來自 8.3 reorder

如其名,可以把預設的 order override 掉。

class Post < ActiveRecord::Base
  has_many :comments, :order => 'posted_at DESC'
end

Post.first.comments.reorder('posted_at ASC')

joins + where 的精美語法

來自 11.3 Specifying Conditions on the Joined Tables

Client.joins(:orders).where('orders.created_at' => time_range)

改成精美的語法

Client.joins(:orders).where(:orders => {:created_at => time_range})

Scopes

來自 13 Scopes

簡單來說就是

class Post < ActiveRecord::Base
  scope :published, where(:published => true)
end

就可以用類似這樣的語法。

Post.published
@board.posts.published # 假設 board has_many posts

scope 會回傳 ActiveRecord::Relation object,所以你還可以串接,例如

Post.published.highlight.where(:pinned => true)

由於我讀的是 Rails 3.2 的版本,所以還是舊語法,Rails 4 已經強制改成

class Post < ActiveRecord::Base
  scope :published, -> { where(:published => true) }
end

可以傳入參數的 scope

來自 13 Scopes > 13.2 Passing in arguments

可以到來源查看 scope + lambda 允許傳入參數的手法,但即使是 Rails 4 的 Guides 仍不推薦該手法,而是推薦:

class Post < ActiveRecord::Base
  def self.before(time)
    where("created_at < ?", time)
  end
end

Post.before(Time.zone.now)

這種寫法一樣可以支援串接

Category.first.posts.before(Time.zone.now)

default_scope

來自 13 Scopes > 13.4 Applying a default scope

有時候想預設就套用 scope,可以用 default_scope

取消 default_scope

來自 13 Scopes > 13.5 Removing all scoping

用 default_scope 之後就會有個問題:某個管理功能需要取得 全部 的資料,甚至有時候是用別人的外掛、別人的code 繼承下來的等狀況,只想直接跳過 default scope 的話。可以用:

Client.unscoped.all

find_by_xxx 加驚嘆號後的行為

來自 > 14 Dynamic Finders

id = 123
find(id)        # 找不到 123 會炸 Exception
find_by_id(id)  # 找不到 123 會回傳 nil
find_by_id!(id) # 找不到 123 會炸 Exception

找不到符合條件的就 new/create

來自 > 15.1 first_or_create

Client.where(:first_name => 'Andy').first_or_create(:locked => false)

除了 create 外,還有其他的選擇

  • first_or_initialize 相當於 new
  • first_or_create! invalid 的時候會炸 exception

直接下 SQL, 回傳 hashes

來自 > 17 select_all

find_by_sql 可能蠻多人知道的,但是他會轉成 model instance。如果想要各欄位的 hash,可以用 select_all

Client.connection.select_all("SELECT * FROM clients WHERE kind = '1'")

他會回傳類似

[{"id"=>57, "kind"=>"1", "created_at"=>2013-11-16 14:09:44 +0800, "updated_at"=>2013-11-16 14:09:44 +0800}, {"id"=>57, ... }, ...]

pluck 取出某欄位的值成為一個陣列

來自 > 18 pluck

若只想取出某一欄位的值成為陣列,直覺的想法可能是:

User.select(:id).where(:actived => true).map { |c| c.id }

但如果你之後沒有要使用物件的話,這麼做其實白耗了 instantiate 的時間。

User.where(:actived => true).pluck(:id)

因為 pluck 不會 initialize,速度會差非常多。根據這篇 Getting to Know Pluck and Select ,一萬筆資料可以差到 10 幾倍 (1.11秒 vs 0.08秒)。

.any? 跟 .many?

來自 > 19 Existence of Objects

["a"].any?            # => true
["a"].many?           # => false
["a", "b", "c"].any?  # => true
["a", "b", "c"].many? # => true

關連物件跟 scope 也可以這樣用

Post.recent.any?
Post.recent.many?

但如果只是要檢查是否存在,沒有更多的動作的話,.exists? 會比 .any? 理想,請參考 閱讀筆記 part 1