Tik’s Blog

ร้อยแปดพันเก้า

ผจญภัยใน will_paginate เพื่อเพิ่ม first_item ใน RailsSpace (ภาค 3)

leave a comment »

ต่อจากตอนที่แล้ว…ว่า Rails เหมือนกับจะไม่โหลด class WillPaginate::Collection จาก gem ขึ้นมาเพื่อ extend definition ที่เราสร้างไว้ใน lib/will_paginate/collection.rb  เลยทดลองเปลี่ยนโค้ดโดยใช้ class_eval แทน

WillPaginate::Collection.class_eval do
  def first_item
    offset + 1
  end
end

เพราะคิดว่าการเรียกใช้ class จะทำให้ Rails โหลด gem ให้ แต่วิธีนี้มีสองปัญหา คือหนึ่ง Rails  ไม่ได้โหลด gem ให้และสอง การที่ไม่ได้ define class แบบ class Collection ทำให้ Rails ยิง error ว่า

Expected /Users...rails_space/lib/will_paginate/collection.rb to define
  WillPaginate::Collection
/opt/.../activesupport-2.3.2/lib/active_support/dependencies.rb:426:in
  `load_missing_constant'

จาก stack trace เราสรุปได้ว่า Rails  รับผิดชอบในการโหลด dependencies ต่างๆ ไม่ใช่ Ruby เพราะฉะนั้น Rails สามารถที่เลือกโหลดหรือไม่โหลดไฟล์ได้  ข้อสังเกตุที่สองคือวิธีการโหลด Collection class นั้นต่างกับวิธีการโหลด class อื่นๆที่มาจาก gem  ของ Collection class เป็นแบบนี้

/...activesupport-2.3.2/lib/active_support/dependencies.rb:426:in
  `load_missing_constant'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:80:in
  `const_missing'
/...rails_space/lib/will_paginate/collection.rb:1
/...activesupport-2.3.2/lib/active_support/dependencies.rb:380:in
  `load_without_new_constant_marking'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:380:in
  `load_file'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:521:in
  `new_constants_in'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:379:in
  `load_file'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:259:in
  `require_or_load'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:425:in
  `load_missing_constant'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:80:in
  `const_missing'
/...rails_space/lib/will_paginate/collection.rb:1
/...rubygems/custom_require.rb:31:in `gem_original_require'
/...rubygems/custom_require.rb:31:in `require'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:156:in
  `require'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:521:in
  `new_constants_in'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:156:in
  `require'

ส่วนของ class อื่นๆจะประมาณนี้

/...gems/mislav-will_paginate-2.3.8/lib/will_paginate/array.rb:1
/...rubygems/custom_require.rb:31:in `gem_original_require'
/...rubygems/custom_require.rb:31:in `require'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:156:in
  `require'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:521:in
  `new_constants_in'
/...activesupport-2.3.2/lib/active_support/dependencies.rb:156:in
  `require'

จะเห็นได้ว่า แทนที่จะโหลด Collection class จาก gem จริงๆแล้ว Rails พยายามโหลด Collection มาจาก lib/will_paginate/collection.rb ของเราต่างหาก

หลังจากอุตส่าห์บ้าลองนู่นนี่อยู่นาน เพิ่งนึกได้ว่ามีหนังสือ The Rails Way อยู่ เลยลองเปิดดูบทแรก ปรากฏว่านี่มันเป็นพฤติกรรมที่มีมาตั้งแต่ Rails 1.2 แล้วนี่หว่า – -”

คือ Rails จะโหลด class หรือ module ที่ยังไม่ถูกโหลดขึ้นมาโดยอัตโนมัติ โดยพยายามหาจาก load_paths ที่ define ตอน Rails บู๊ตขึ้นมา สำหรับกรณีนี้ สรุปว่าปัญหาน่าจะเกิดจากการที่ /lib อยู่ใน load_paths ก่อน gem path นั่นเอง ลองดูได้จาก

$ script/console
>> $:

(คลื่นแทรก: ลอง google ดู rails auto-loading ปรากฏว่ามีคนเจอปัญหาแบบเดียวกันเดี๊ยเลย ที่นี่)

เอาล่ะ หลังจากบ้าๆบอๆมานาน ก็ถึงวิธีแก้ เพราะไม่อยากมีภาคต่อแล้ว มันยาวเกิน

วิธีแก้วิธีแบบแรกก็คือใช้โค้ดแบบในลิงก์คนที่เจอปัญหาแบบเดียวกันนี่แหละ เพิ่มโค้ดนี้ต่อท้ายสุดใน environment.rb

module WpCollectionExtensions
  def first_item
    offset + 1
  end
end

WillPaginate::Collection.instance_eval { include WpCollectionExtensions }

แบบทีสองคือใช้ after_initialize ของ config ใน environment.rb ประมาณว่าเพิ่มโค้ดนี้ก่อนจบ block ใน  environment.rb วิธีนี้คือการเพิ่ม hook ให้ Rails รันโค้ดใน block ที่ส่งไปหลัง initialize ทุกอย่างเสร็จเรียบร้อย

  config.after_initialize do
    class WillPaginate::Collection
      def first_item
        offset + 1
      end
    end
  end

จริงๆแล้วสองวิธีนี้มันก็ไม่ต่างกันเท่าไรหรอก เพราะว่ามันรอจน Rails บู๊ตจบกระบวนความแล้วก็เอาโค้ดมาใช้

วิธีที่สามคือสร้างไฟล์ไว้ใน config/initializers แล้วเอาโค้ดไปใส่ไว้ในนี้แทน เพราะ Rails โหลดไฟล์ที่มีนามสกุล .rb ใน initializers นี้ด้วย เช่น สร้างไฟล์ชื่อ config/initializers/extend_will_paginate.rb แล้วใส่โค้ดเดียวกันก็ได้

    class WillPaginate::Collection
      def first_item
        offset + 1
      end
    end

เป็นอันจบซีรี่ส์โคตรยาวซะที จริงๆคงมีวิธีอื่นอีก แต่แค่นี้ก็เวิร์คแล้ว ไม่รู้จะหามันเพิ่มไปทำไมอีก

Written by sukita

เมษายน 3, 2009 ที่ 11:35 pm

บันทึกโพสใน Programming

Tagged with

ใส่ความเห็น