分類
Performance Programming Rails

最佳化 ActiveRecord SQL 查詢

Update(2008/9/5) 補充 named_scope 也可以用在 :select,感謝 tsechingho++

要看 AciveRecord 產生的 SQL 是什麼,除了可以直接 tail -f log/development.log 之外,也可以在 script/console 的情況下透過以下指令直接把 log 直接顯示出來:

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.clear_active_connections!

如此便可以好好在 console 的環境下實驗 ActiveRecord 囉。

我看到關於 ActiveRecord SQL 查詢最佳化方式有這幾種:

1. 使用 :select

只撈需要用到的欄位,特別是如果不需要用到的 text 或 binary 欄位請排除。

Event.find(:all, :select => "id, title, description")

搭配 named_scope 我們可以把常用的 :select 預先設定好,例如:

class User < ActiveRecord::Base named_scope :short, :select => "id, name, email"
end

User.short.find(:all)

2. 使用 :include

使用 :include 避免 N+1 次 queries 的問題。


@events = Event.find(:all, :include => [:group] )


@events.each do |e|
e.group.title
end

如果沒有加 :include 把相關的 groups 一起載入,在迴圈中就會產生 @event.size 次對 group 的個別 SQL 查詢,會非常傷。加了 :include 之後總共只需查詢兩次。

另外一個比較少人知道的是,在設定 Model associations 時,如果有很明顯的情境一定會順道載入二階 association model,可以設定 :include 在 has_many, belongs_to, has_one 上面,例如:


class User < ActiveRecord::Base has_one :foo, :include => [:bar]
end

如此便會在載入 @user.foo 的同時,也會提早載入 @user.foo.bar。

3. 資料庫索引

針對 foreign key 要加上資料庫索引 index。在 migration 上透過 add_index 就可以加上去了。

4. 特定情況下可用 :joins 取代 :include

在只需要用到 :conditions 而不需要載入該 model 的情況下,可以用 :joins 取代 :include。

Group.find(:all, :include => [ :group_memberships ], :conditions => [ "group_memberships.created_at > ?", Time.now - 30.days ] )

因為其中會載入 group_memberships model 只是為了加條件式,而沒有要撈出裡面的資料,所以可以改用 :joins,這其中的差異你看產生出的SQL就知道了 :)


Group.find(:all, :joins => [ :group_memberships ], :conditions => [ "group_memberships.created_at > ?", Time.now - 30.days ] )

5. 自己寫 SQL

ActiveRecord 可以直接寫 find_by_sql。

6. denormalization 逆正規化

當資料非常多,又要常常查詢其中計算的結果,這時可以考慮使用逆正規化的手法,將計算的結果也當做資料存起來。

一個最常見最基本的用法就是計算總數了,例如以下的例子可以解決需要常常查詢 @topic.posts.size 該篇主題有多少文章的情境:


class Topic < ActiveRecord::Base has_many :posts end class Posts < ActiveRecord::Base belongs_to :topic, :counter_cache => true
end

我們會在 Topic model 新增一個欄位 posts_count,然後 :counter_cache 就會幫我們在新增刪除 Post 時,自動更新所屬 Topic 的 posts_count 欄位資料。這時只要打 @topic.posts.size 就會直接回傳 posts_count,而無需每次都再發一個 SQL query 去 count 有多少筆 posts。

其他逆正規化的方式也包括了統計結算報表等,我們把需要複雜計算的的結果(可以透過非同步的機制如 cron 定期去觸發) 存到 Report model,這樣使用者直接撈 Report 的結果即可。

分類
Programming Rails

小探 Rails ActiveSupport

Update(2008/4/9): 這篇 RAILS RUBYISMS ADVENT也可以一看。

ActiveSupport 是 Rails 的工具箱。最近在看 Advanced Rails, O’Reilly 一書,有幾樣東西值得記上一筆:

JSON

我們有 Object#to_json,物件如 array,hash 等都可以呼叫 to_json 轉 JSON 格式,非常方便與 JavaScript 做銜接。

Blank

所有的物件都加上了 blank? 這個函式,回傳 true 如果是 1. 空字串 2. 只含空白的字串 3. false 4. nil 5. empty array [] 6. empty hash {}。所以別再寫 ( s.nil? || s.empty? ) 啦。

Class Attribute Accessors

可用宣告的方式定義 Class Attribute,如

  class Foo
     cattr_accessor :bar
     self.bar = ""
  end

這樣會定義出來的 C.bar 即 @@bar

Class Inheritable Attributes

Class Attribute 是整個類別繼承體系共用,這在我們寫 ActiveRecord 相關 plugin 時非常不適用,因為所有的 model 都繼承自 ActiveRecord,但是各自又要有不同的 Class Attributes 值。最常見的使用狀況就是 plugin 了,model A 和 model B 都 include 某個 plugin,但是這個 plugin 的設定值要不一樣。拿大家都在用的 attactment_fu 舉例:

class UploadImage < ActiveRecord::Base
  has_attachment :content_type => :image, :storage => :file_system
end

翻出 has_attachment 的 source code 你就看到這招了:

def has_attachment(options = {})
      ......
     class_inheritable_accessor :attachment_options
     self.attachment_options = options    # 這裡的 self 指的是 UploadImage
      ......
end

除了 class_inheritable_accessor(syms) ,還有 class_inheritable_array(syms) 和class_inheritable_hash(*syms) 等。

Class Attribute Accessors 的原理可以請參考 class instance variables 這篇。

Date and Time conversions

不需要每次寫 Helper 用 strftime,我們可在 environment.rb 新增自訂的 format,例如

 ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!( :foo => '%m/%d %l:%M %p')

這樣就可以對 Time 物件呼叫 to_s(:foo),內建還有 :default, :short, :long, :db 等等。

alias_method_chain

在 Rails source code 十分常見:

  alias_method_chain :target, :feature

等同於

  alias_method :target_without_feature, :target
  alias_method :target, :target_with_feature

Delegation

將 methods 傳給另一個 object

  class Account < ActiveRecord::Base
      has_one :foo
      delegate :free?, :to => :foo
  end

這樣 account.free? 就會呼叫 account.foo.free? 考慮 foo 可能 nil,我們可以多一個檢查:

  delegate :free?, :to => "something.nil? ? false : something"

甚至兩層,假設 foo 有 bar:

  delegate :free?, :to => "foo.bar"

這樣 account.free? 就會呼叫 account.foo.bar.free?

#Object#returning

讓你執行一些操作然後傳回:

 returning(User.new) do |u|
   u.name = "Foo"
 end

#Object#with_options

最常用在 routes.rb,不過其實任意物件都可以用,他會將參數自動 merge 到 method call 的 hash 參數:

 map.with_options( :controller => "people" ) do |p|
   p.root :action => "index"
   p.about :action => "about"
 end
分類
JavaScript Programming Rails

FaceboxRender: Rails 無縫使用 lightbox

FaceboxRender 是我最近寫的第二個 Ajax UI plugin,它讓你在 Rails 中可以無縫使用 lightbox 效果,搭配使用的 Javascript library 正如其名是 Facebox。首先你得先裝好 jQuery 和 Facebox:

  1. 下載 jQuery (建議你可以考慮用 jRails 完全換掉 Prototype library)
  2. 下載 facebox
  3. 把 facebox js file 放到 /public/javascripts/
  4. 把 facebox css file 放到 /public/styleshees/
  5. 把 facebox all image files 放到 /public/facebox/
  6. 設定好 layout 的 stylesheet_link_tag 和 javascript_include_tag 加入這些 js,css 檔案

facebox-render 目前放在 github,請下載後放到 vender/plugins/facebox_render

然後在你要使用的 Controller 加入以下程式碼

 include FaceboxRender

或是你直接放到 /app/controllers/application.rb 裡面也是可以。

回到 view code,當你要叫出一個 lightbox :

 facebox_link_to "Login", :url => login_url

或是你也可以用 remote_link_to, remote_form_tag 等Rails內建的 Ajax Helper,差別在於 facebox_link_to 會先秀出一個 Loading lightbox,然後再送 Ajax request。

重頭戲是 action :


def new
 # do some thing you want
 respond_to do |format|
  format.html
  format.js { render_to_facebox }
 end
end

這裡巧妙的運用了 respond_to 來處理 ajax request,預設的 render_facebox 會 render 不帶 layout 的HTML(即 new.html.erb)。當然我們也支援傳入 :action, :template, :partial 等常見的Rails參數,或是乾脆傳入 :html 也可以。

傳入 :msg 的話,會插入一段 <div class=”message”>your msg</div> 到裡面去,方便你做一些提示或警告訊息。

除了 render_facebox,也有 close_facebox 可以關掉 lightbox。也可以用 redirect_from_lightbox 重新讀取另一頁。

實際的應用非常方便,你可以在網頁中用 remote link 或是 remote form submit 叫出 facebox。在 facebox 中也還可以再放 remote link 或 remote form submit,接收處理的 action 可用 render_facebox 或關掉 close_facebox 抑或重新讀取頁面 redirect_from_facebox。

FaceboxRender 的隨附文件請見 README

P.S. 這篇文的英文版在 Handlino’s blog

分類
Programming Rails

Rails Email 同時提供 plain 及 html 版本

讓 Email 同時提供 text/plain 跟 text/html 版本,讓不支援 HTML 的 Email client 也可以顯示 plain 是一種是具有親和力的作法。根據 multipart-alternative 的定義,正確的格式應該長這樣,用 boundary 分隔成兩個部份:

   From:  Nathaniel Borenstein <nsb@bellcore.com>
   To: Ned Freed <ned@innosoft.com>
   Subject: Formatted text mail
   MIME-Version: 1.0
   Content-Type: multipart/alternative; boundary=boundary42

   --boundary42

   Content-Type: text/plain; charset=utf-8

   ...plain text version of message goes here....

   --boundary42
   Content-Type: text/html; charset=utf-8

   .... html version of same message goes here ...

   --boundary42--

分類
Programming Rails

Rails L10n 多國語言方案

如果是需要多國語言,我在 registrano.com 是使用Globalize plugin,它是把翻譯資料放資料庫,因此還可以做個網站後台來修改,非常方便。語法則是

"Hello, World!".t

我個人是蠻喜歡這種 method call 寫法。不過它也提供與 gettext 一樣的語法:

_("Hello, World!")

下載請前往 Github

另一個更簡單不需要資料庫的 plugin 是 Gibberish,它使用 key-value 方式跟 yml 檔案儲存,語法是

"Hello, World!"[:welcome]

如果不是要多國,而只是想要一國(如中文),則可以考慮裝 Localization SimplifiedLocalization Plugin,前者可以處理 Rails 預設的錯誤訊息跟日期格式等。後者則是因為我自己有個需求是 RHTML view code 可以用中文沒關係,但是我在 controller 或 model 裡蠻不想打中文的(一來是我不知道客戶要的確切文字,也不想讓人家直接去改 controller。二來是 Textmate 不支援中文),所以需要一個最簡單的 L10n 機制來幫忙。

採用 Localization Simplified 後,發現還是碰到 Model attribute name 無法順利中文化,例如出現 “Password 不能是空白” 這樣的錯誤訊息。目前想到這樣 hacking:

#put this in /config/environment.rb
class String
  def humanize
      _( self.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize )
  end
end

於是就可以跟 Localization Plugin 接軌了 :>

分類
Programming Ruby

一些 Ruby Dynamic Features 記事 (3) define_method

Update(2010/4/15): self, current class 與 class_eval, instance_eval 的關係

define_method 讓函式名稱也可以參數化。