Saturday, February 25, 2006

Rails "with_scope"

ใน ActiveRecord มีคำสั่งหนึ่งที่ชื่อ with_scope
คำสั่งนี้ช่วยให้เราจัดการกับ business logic ประเภท

user A ที่สังกัดหน่วยงาน 1001 เมื่อ login เข้ามาแล้ว
สามารถ query ข้อมูลได้เฉพาะข้อมูลของหน่วยงาน 1001 เท่านั้น
และเมื่อสร้าง transaction,
transaction นั้นจะมีรหัสหน่วยงานที่รับผิดชอบเป็น 1001

หรือไม่ก็

User B มี authorize แบบ Regulator
ดังนั้นจึงสามารถ grant สิทธิให้กับเฉพาะ user
ที่สังกัดอยู่ภายใต้ Company ที่ user B ดูแลอยู่


ลองดู controller code ของตัวอย่างที่ 1
กรณีที่เขียนแบบไม่มี with_scope

def list
@txs = Tx.find(:all,
:conditions =>
["organize_id = ?", session[:user].organize_id])
end

def create
...
# tx ที่สร้างจะสังกัดหน่วยงาน ตามหน่วยงานของ user
tx.organize_id = session[:user].organize_id
...
tx.save
...
end


ถ้าใช้ with_scope เข้ามาช่วย

def list
Tx.with_scope(:condition => "organize_id = #{session[:user].organize_id}") do
@txs = tx.find(:all)
end
end

def create
Tx.with_scope(:create => {:organize_id = session[:user].organize_id}) do
...
Tx.save
end
end


ถ้าให้สวยขึ้นอีก ก็ให้แยกส่วนที่ซ้ำๆออกมา

protected
def org_scope
{
:find => {:condition => "organize_id = #{session[:user].organize_id}",
:create => {:organize_id = session[:user].organize_id}
}
end

public
def list
Tx.with_scope(org_scope) do
Tx.find(:all)
end
end

def create
Tx.with_scope(org_scope) do
...
end
end


Note: เริ่มพบว่า Rails api เริ่มไม่ consistency เยอะขึ้นเรื่อยๆ เช่น
กรณี find โดยตรงเราสามารถทำแบบนี้ได้

@txs = Tx.find(:all,
:conditions =>
["organize_id = ?", @org_id])

แต่ถ้าใช้ผ่าน with_scope จะ call แบบนี้ไม่ได้

Tx.with_scope(:find =>
{:conditions =>
["organize_id = ?", @org_id]}) do
Tx.find(:all)
end


สาเหตุก็คือ ใน method add_conditions ใน file "base.rb" ของ ActiveRecord
มีการใช้ sanitize_sql (method ที่รับผิดชอบการแปลงแบบที่กล่าวข้างบน) เฉพาะกับ
:condition ที่ส่งมากับคำสั่ง find เท่านั้น
ไม่ได้ใช้กับ :condition ใน with_scope ด้วย

940 def add_conditions!(sql, conditions)
941 puts "add_conditions #{sql}, #{conditions}"
942 segments = [scope(:find, :conditions)]
943 segments << sanitize_sql(conditions) unless conditions.nil?
944 segments << type_condition unless descends_from_active_record?
945 segments.compact!
946 sql << "WHERE (#{segments.join(") AND (")}) " unless segments.empty?
947 end

ทางแก้ ก็อาจจะ patch base.rb
เพิ่ม sanitize_sql เข้าไปครอบส่วน scope

940 def add_conditions!(sql, conditions)
941 puts "add_conditions #{sql}, #{conditions}"
942 segments = [sanitize_sql(scope(:find, :conditions))]
943 segments << sanitize_sql(conditions) unless conditions.nil?
944 segments << type_condition unless descends_from_active_record?
945 segments.compact!
946 sql << "WHERE (#{segments.join(") AND (")}) " unless segments.empty?
947 end


Note: เข้าไป check source code ของ Rails
มีคน patch ส่วนแก้ไขเข้าไปแล้ว อยู่ใน change set [3379]

Related link from Roti

No comments: