Tik’s Blog

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

Strong-typed DataSet เป็นอันตรายสำหรับสถาปัตยกรรมระบบ

leave a comment »

สำหรับใครก็ตามที่ต้องทำงานกับระบบที่มีขนาดย่อมขึ้นไปจนถึงใหญ่ ควรหลีกเลี่ยง strong-typed DataSet/DataAdapter ให้ไกลที่สุดเท่าที่จะเป็นไปได้ เพราะ strong-typed DataSet:

  1. ไม่เหมาะกับการสร้าง domain model ที่ดีเหมือน PONO และไม่ natural เพราะมันเป็นเหมือน container ขนาดใหญ่ที่ปนเรื่อง domain กับ persistence ไว้ด้วยกัน (DataTable ไม่ใช่ domain model)
  2. ความที่เป็น container ขนาดใหญ่ทำให้มีต้นทุนการ serialize/deserialize สุง (และจะยิ่งน่าเบื่อเมื่อ DataSet คุณมีประมาณ 100 table) ทั้งตอนพัฒนาด้วย designer และตอนใช้งานใน form (บางครั้งการสร้าง DataSet ขนาดใหญ่เพื่อที่จะใช้แค่ไม่กี่ตารางใช้เวลามากกว่าการ load form เสียอีก)
  3. ทำให้ต้องยึดติดกับ designer และ code generator ที่ทำงานช้า (สำหรับคนที่ไม่มีเครื่องแรง ๆ)
  4. ทำให้ขาดความยืดหยุ่นในการทำงานกับ database อื่น ๆ และยากที่จะ decouple เรื่อง persistence ออกมา ถ้าคุณต้องการเปลี่ยน DB คุณต้อง generate DataSet ใหม่ และไปแก้ไข compile error ที่อื่น ๆ อีกด้วย
  5. ไม่เหมาะกับการทำงานใน application layer (service layer) เพราะ strong-typed TableAdapter ไม่มีอะไรที่ expose เรื่องการจัดการ transaction แบบ conversation (ถ้าใช้ TransactionScope อาจจะปวดหัวมากขึ้นกับปัญหาอื่น เช่นการ configure DTC เป็นต้น)

สรุปได้ว่า การใช้ strong-typed DataSet เป็นศูนย์กลางของสถาปัตยกรรมระบบเพื่อแทน model และ data layer ทำให้ระบบขาดความยืดหยุ่นในระยะยาว

ถ้าคุณเปลี่ยน model เป็น PONO หลังจาก DataSet คุณมีประมาณ 100 ตาราง ก็แทบไม่ต่างอะไรกับการ rewrite ระบบใหม่ เพราะเปลี่ยน model ก็ต้องเปลี่ยนเรื่อง persistence และต้องเปลี่ยน UI อีกต่างหาก (ไม่ว่าจะเป็นการเรียกใช้ data layer หรือ data binding กับ model)

คำแนะนำ: หลีกเลี่ยง strong-typed DataSet หรือเปลี่ยนไปใช้สถาปัตยกรรมที่อิงกับ PONO ให้เร็วที่สุดก่อนที่ DataSet จะมีขนาดใหญ่เกินไป

Written by sukita

มกราคม 29, 2010 at 11:03 am

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

Spring: TypeInitializationException เวลาเรียก GetContext()

leave a comment »

ถ้าคุณเจอ TypeInitializationException แบบนี้เวลาเรียกใช้ ContextRegistry.GetContext():

An unhandled exception of type 'System.TypeInitializationException' occurred in WindowsApplication1.exe

Additional information: The type initializer for 'Spring.Context.Support.ContextRegistry' threw an exception.

ลองเช็คดูว่ามีการใช้ PropertyPlaceholderConfigurer หรือเปล่า? ถ้ามี ให้เพิ่ม handler สำหรับ section เข้าไปใน app.config ด้วย:

<configSections>
  ...
  <section name="databaseSettings"
           type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</configSections>

Written by sukita

มกราคม 1, 2010 at 4:38 pm

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

Tagged with ,

Spring.NET: ทำไมต้อง virtual สำหรับ method ที่มี [Transaction] attribute

leave a comment »

กรณีที่ class มี method ที่ใช้ [Transaction] attribute และ class นั้นไม่มี interface เราต้อง declare method นั้นเป็น virtual เช่น

// Class does not define an interface.
public class CustomerDao
{
  // We must declare this transactional method as virtual.
  [Transaction]
  public virtual void Save(Customer obj)
  {
    // Save to database.
  }
}

ที่จริงเราต้อง define ทุกๆ method และ property ที่ต้องการให้ Spring ใช้เป็น virtual ด้วยซ้ำ เพราะว่า Spring สร้าง class proxy โดยการสร้างอีก class นึงที่ inherit มาจาก CustomerDao และ override ทุก public method/property ที่สามารถ override ได้ (Decorator pattern) เพื่อเพิ่ม advice เข้าไป ส่วน method ที่ไม่ใช่ virtual ไม่สามารถถูก override ได้ เพราะฉะนั้น Spring ก็จะไม่ยุ่งอะไรกับ method พวกนี้

[Transaction] attribute ไม่สามารถทำงานได้กับ method ที่ไม่มี virtual เพราะ Spring ไม่สามารถเพิ่ม transaction advice ครอบ method นั้นๆ ได้

ลองอ่านเพิ่มเติมได้ที่หัวข้อ 13.5.4 Proxying Classes

Written by sukita

ธันวาคม 30, 2009 at 3:43 pm

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

Tagged with

Spring.NET : NullReferenceException เวลาใช้ field ของ dependency พร้อมๆ กับ Transaction

leave a comment »

สมมุติว่าเราตั้งค่า object อันนึงใน Spring.NET ชื่อ dataService ซึ่งใช้อีก object นึงชื่อ adoTemplate ทั้งสอง object ถูกตั้งค่าไว้แล้ว

<object id="adoTemplate" type="Spring.Data.Core.AdoTemplate, Spring.Data">
  <property name="DbProvider" ref="dbProvider"/>
</object>
<object id="dataService" type="MyApp.DataService, MyApp">
  <property name="AdoTemplate" ref="adoTemplate"/>
</object>

DataService เป็น class ใน service layer ธรรมดา ไม่มี interface ไม่มี base class อะไร

[Service]
public class DataService
{
  private AdoTemplate adoTemplate;

  public AdoTemplate AdoTemplate
  {
    set { adoTemplate = value; }
  }

  [Transaction]
  public void MyMethod()
  {
    // This code does not work.
    this.adoTemplate.Execute(...);
    this.adoTemplate.Execute(...);
  }
}

โค้ดด้านบนเป็นโค้ดที่ผิด เพราะสองสาเหตุ

สาเหตุแรกคือ เราไม่ได้ใช้ property AdoTemplate แต่ใช้ field ที่ชื่อ adoTemplate ใน MyMethod ที่ถูกแล้วเราควรใช้ property เพราะฟิลด์นี้อาจไม่ถูก set ค่าเวลา Spring สร้าง proxy ผลที่เกิดก็คือเราจะเจอ NullReferenceException เวลาถึงบรรทัด adoTemplate.Execute(…)

ข้อที่สองคือ เราไม่ได้ประกาศ AdoTemplate เป็น virtual ทำให้ Spring ไม่ได้ set ค่าของ AdoTemplate property เวลาสร้าง context

สรุปแล้ว โค้ดที่ถูกต้องคือ

[Service]
public class DataService
{
  private AdoTemplate adoTemplate;

  // Don't forget to make this virtual.
  public virtual AdoTemplate AdoTemplate
  {
    set { adoTemplate = value; }
  }

  [Transaction]
  public void MyMethod()
  {
    // Use property instead of field.
    AdoTemplate.Execute(...);
    AdoTemplate.Execute(...);
  }
}

Written by sukita

ธันวาคม 30, 2009 at 2:17 pm

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

Tagged with

Spring.NET : Could not get ADO.NET connection.

leave a comment »

ถ้าเจอ error นี้ให้เช็ค connection string ว่าผิดหรือเปล่า

Written by sukita

ธันวาคม 30, 2009 at 1:59 pm

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

Tagged with

Use List instead of HashSet for NHibernate 2.1 idbag Mapping

leave a comment »

If you’re mapping an <idbag> with <composite-element> in NHibernate 2.1 and are getting an exception similar to this:

Collection cannot be null.
Parameter name: c
at System.Collections.ArrayList..ctor(ICollection c)
at NHibernate.Collection.PersistentIdentifierBag..ctor(ISessionImplementor session,
    ICollection coll)
at NHibernate.Collection.Generic.PersistentIdentifierBag`1..ctor(ISessionImplementor session,
    ICollection`1 coll)
at NHibernate.Type.GenericIdentifierBagType`1.Wrap(ISessionImplementor session,
    Object collection)
at NHibernate.Event.Default.WrapVisitor.ProcessArrayOrNewCollection(Object collection,
    CollectionType collectionType)

See if you’re using System.Collections.Generic.HashSet for your <idbag> field. If so, change the implementation class to System.Collections.Generic.List and that should fix the problem.

HashSet is for mapping a <set> semantics, not suitable for an <idbag>.

Written by sukita

ธันวาคม 7, 2009 at 2:06 pm

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

Tagged with

ArgumentError in Community#search ใน RailsSpace

leave a comment »

สงครามยังไม่จบอย่าพึ่งนับศพทหาร…แก้บั๊กที่แล้วไปแป๊ปเดียว เจออันใหม่อีกแล้ว

หลังจากเราแก้ไขให้ will_paginate ทำงานได้ ปรากฏว่าพอทำตาม Listing 11.9 หน้า 336 ก็เจอปัญหาใหม่ ประมาณว่าพอเราค้นหาคำปุ๊ปจะเจอ error ว่า

ArgumentError in Community#search
  Showing app/views/community/_user_table.html.erb where line #16 raised:
  The @community variable appears to be empty. Did you forget to pass the collection
    object for will_paginate?

จริงๆแล้วโค้ดตรงนี้ก็ไม่ใช่ปัญหาเท่าไร  ปัญหาจริงๆอยู่ที่ว่าเราต้องใช้โค้ดใน Listing 11.10 เพื่อเพิ่ม method ให้ทำงานได้ แต่ว่า Listing 11.10 มันเข้ากันไม่ได้กับ will_paginate และ Paginator ก็ไม่มีใน Rails 2.3 แล้วด้วย เพราะงั้นเราต้องเขียนโค้ดทดแทนที่ทำงานกับ will_paginate ได้แทน

คุ้ยๆโค้ด will_paginate ดู ได้โค้ดทดแทนก็ประมาณนี้ (โค้ดค่อนข้างห่วย แต่เวิร์คไปก่อน + โค้ดนี้เป็นโค้ดต่อท้ายใน method search ของ community_controller.rb เพราะยังไม่รู้จะทำให้เป็น generic ยังไง)

def search
  @title = "Search RailsSpace"
  if params[:q]
     ...

    # Now combine into one list of distinct users sorted by last name.
    ...

    # Manual pagination.
    hit_specs = @users.collect { |user| user.spec }
    page = params[:page] || 1
    @specs = WillPaginate::Collection.create(page, 5, hit_specs.count) do |pager|
      pager.replace(hit_specs)
    end
    @users = WillPaginate::Collection.create(page, 5, @users.count) do |pager|
      pager.replace(@users)
    end
    @specs = @specs.paginate(:page =&gt; page, :per_page =&gt; 5)
    @users = @users.paginate(:page =&gt; page, :per_page =&gt; 5)
  end
end

เท่านี้ก็เรียบร้อย กล้อมแกล้มให้ทำงานได้ไปก่อน ค่อยหาวิธีทำให้โค้ดสวยขึ้นเมื่อความรู้เพิ่มขึ้น

อธิบายโค้ดคร่าวๆ:

  • สร้าง @specs ขึ้นมาเพราะ _user_table partial ต้องใช้ส่งให้ will_paginate และเซ็ตค่าให้เป็น specs เฉพาะในหน้าที่ระบุเท่านั้น (เพราะ first_item และ last_item ใน _result_summary partial ต้องใช้)
  • สร้าง @users และเซ็ตค่าให้เป็นเฉพาะ users ที่อยู่ในหน้าที่ระบุเท่านั้นเพราะ _user_table partial ต้องใช้ คล้ายๆกับ @specs แหละ
  • method create ของ WillPaginate::Collection จะทำการสร้าง collection ใหม่ที่จะครอบ @specs/@users โดยสนับสนุนการแบ่งหน้าในตัว (method paginate ก็เป็น method ที่ define ใน will_paginate gem)

Written by sukita

เมษายน 8, 2009 at 10:13 pm

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

Tagged with

ผจญภัยใน 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 at 11:35 pm

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

Tagged with

ร้อยแปดพันเก้า ผจญภัยใน will_paginate เพื่อเพิ่ม first_item ใน RailsSpace (ต่อ)

leave a comment »

ในโพสก่อนหน้า เนื่องจากง่วงเลยหยุดไว้ตรงการสร้าง class WillPaginate::Collection ไว้ใน lib/will_paginate/collection.rb ไว้ประมาณนี้:

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

ซึ่งการวิเคราะห์ตอนง่วงนอนก็เหมือนจะใกล้เคียงอยู่ครึ่งนึง แต่ตอนนั้นยังไม่เข้าใจว่าทำไมเราถึงไม่สามารถ extend class ที่อยู่ในปลั๊กอินที่เป็น gem ได้ เลยลองนั่งไล่โค้ดใน Rails 2.3 เพิ่มเติม ได้มาซึ่งความน่าจะเป็นใหม่ แต่ก่อนอื่นต้องลองลำดับความเข้าใจก่อน

ใน Ruby เราสามารถ define class หรือ method ทับ class/method ที่มีอยู่แล้วได้ เช่น ถ้าเราเขียนโค้ดแบบนี้

module My
  class Test
    def oops
      puts "oops"
    end
  end
end

obj = My::Test.new
obj.oops

class My::Test
  def oops
    puts "oops, i did it again"
  end
end

obj.oops # prints "oops, i did it again"

ผลลัพธ์ที่ได้คือ “oops, i did it again” เพราะว่า Ruby อนุญาติให้เรา override method ได้

ปัญหาคือ ถ้า Ruby อนุญาติให้เรา override class definition ได้ ทำไมเราถึงไม่สามารถทำได้ใน Rails ล่ะ? เพราะสิ่งที่เราทำก็แค่พยายาม redefine WillPaginate::Collection ที่มาจาก gem และเพิ่ม method ใหม่เข้าไปนี่?

คำถามแรกคือ ตอนที่ Rails process ไฟล์ collection.rb ที่เราสร้างขึ้นมานั้น will_paginate gem ได้ถูกโหลดขึ้นมาใช้หรือยัง? เพราะถ้ามันถูกโหลดขึ้นมาใช้งานแล้ว ในทางทฤษฏีเราน่าจะสามารถ override class definition ได้ เพราะฉะนั้นถ้าเราคอมเม้นต์โค้ดที่มีอยู่ทั้งหมดในไฟล์ collection.rb และเพิ่มบรรทัดนี้เข้าไป:

puts defined? WillPaginate::Collection

เราก็จะพบคำตอบว่า…will_paginate ยังไม่ถูกโหลดขึ้นมาใช้งาน ตอน Ruby process ไฟล์นี้ (ลองดูใน log ของ script/server หรือ script/console มันจะพิมพ์บรรทัด nil ซึ่งเป็นผลลัพธ์จากโค้ดบรรทัดนี้นั่นเอง)

นี่หมายความว่าถ้าเรา define WillPaginate::Collection ไว้ใน lib/will_paginate/collection.rb เราก็จะสร้าง class ใหม่ขึ้นมา โดย class นี้จะมีแค่ method first_item ที่เรา define เอาไว้ ทำให้เกิด NoMethodError ที่เกิดขึ้นในโพสก่อนหน้านี่เอง

สมมุติว่าเราสันนิฐฐานว่า Rails ไม่โหลด will_paginate ก่อน process ไฟล์ collection.rb มาลองอะไรเล่นๆกันอีกหน่อย เรามาลอง require ‘will_paginate’ กันใน collection.rb ก่อนจะ redefine class ดูว่าจะเกิดอะไรขึ้น

require 'will_paginate'
puts defined? Spec.paginate

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

หลังแก้โค้ดเมื่อเรารัน script/server ก็จะมี output เหมือนเดิมคือ nil และถ้าเปิดเบราเซอร์ไปที่หน้า community และเลือกซักตัวอักษรนึงก็จะเจอ error แบบเดียวกันในโพสที่แล้วเลย คือ

NoMethodError in CommunityController#index
  undefined method `create' for WillPaginate::Collection:Class

ดูเหมือนว่าการ require will_paginate ก็ไม่โหลด definition ของ Collection จาก gem แต่ปัญหาคือเราสามารถเรียกใช้งาน Spec.paginate ได้ตามปกติใน community_controller.rb ซึ่งหมายความว่า gem ต้องถูกโหลดขึ้นมาแล้วแน่นอน

สรุปข้อมูลที่พบคร่าวๆก่อน:

  1. เกิดการ process ไฟล์ lib/will_paginate/collection.rb ขึ้นตอน Rails initialize
  2. will_paginate ยังไม่ถูกโหลดขึ้นมาตอน Rails process ไฟล์ lib/will_paginate/collection.rb
  3. การ require ‘will_paginate’ ใน collection.rb ก็ไม่โหลด gem ขึ้นมา
  4. gem ถูกโหลดขึ้นมาใช้งานแล้วเมื่อเราเปิดหน้า community page ในเบราเซอร์ (เพราะมีการเรียก paginate method จาก Spec model ได้จริงๆ จึงเกิด error)
  5. gem ที่ถูกโหลดขึ้นมาไม่รู้จัก Collection class ที่ define อยู่ในตัว gem เอง แต่กลับมาใช้ Collection class ที่ define ใน lib/will_paginate/collection.rb

จุดสังเกตุที่ 4 และ 5 น่าสนใจมาก ตอนนี้สันนิฐฐานว่า Rails ไม่ได้โหลด definition จาก gem ถ้า class ถูก define ไว้แล้วในที่อื่นๆ (ในกรณีนี้คือใน lib/will_paginate/collection.rb) อาจจะเป็นเพราะ Rails 2.3 เพิ่มฟีเจอร์การ lazy load/autoload เพื่อประสิทธิภาพการทำงาน

ข้อสันนิฐฐานนี้มีความเป็นไปได้ตรงที่ class อื่นๆของ gem เช่น WillPaginate และ finder extension ก็ถูกโหลดขึ้นมาใช้งาน จะเว้นก็แค่ Collection class จาก gem ที่ไม่ถูกโหลดขึ้นมาเลยทำให้เกิด NoMethodError

สิ่งที่เหลือต้องยืนยันคือว่า จริงหรือเปล่าที่ autoload ใน Rails 2.3 จะไม่โหลด class ที่ถูก define ไว้แล้วซ้ำสองครั้ง ถึงแม้ว่าการโหลดครั้งที่สองจะโหลด class ที่มี method เพิ่มเติมด้วย?

ติดตามตอนต่อไป…

Written by sukita

เมษายน 2, 2009 at 11:49 pm

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

Tagged with

ผจญภัยใน will_paginate เพื่อเพิ่ม first_item ใน RailsSpace

with one comment

ในโพสก่อนหน้าเกี่ยวกับการแบ่งหน้าใน RailsSpace ได้เขียนอัพเดตต่อท้ายว่าไปเจอตัวอย่างของบล๊อก RailsSpace ที่เจ๋งกว่าเพราะว่าเอาโค้ด first_item, last_item, ฯลฯ ไปไว้ใน class WillPaginate เลย ทำให้ไม่ต้องสร้างตัวแปรพิเศษใน view

ปัญหามันอยู่ที่ว่าไอ้โค้ดตัวอย่างในไฟล์​ lib/will_paginate_ext.rb มันเหมือนจะไม่ถูกโหลดขึ้นมาใช้งานใน Rails 2.3 นี่สิ  ไม่รู้ว่าคนอื่นเจอปัญหาเดียวกันหรือเปล่าแต่ว่าถ้าเอาโค้ดไปอยู่ในไฟล์นี้แล้วเรียกใช้ first_item จะเจอปัญหาว่า:

NoMethodError: undefined method `first_item' for #<WillPaginate::Collection:0x291ec20>

เลยลองแก้วิธีแรกด้วยการเปลี่ยนชื่อไฟล์เป็น will_paginate.rb แต่เจอปัญหาใหม่ว่า

Expected /Users/Tik/temp/rails_space/lib/will_paginate.rb to define WillPaginate

อันนี้ก็พอเข้าใจได้ เพราะตาม convention แล้ว Rails คงคาดหวังให้เรามี class ชื่อ WillPaginate ในไฟล์ will_paginate.rb เลยลองใหม่ คราวนี้เปลี่ยนชื่อไฟล์เป็น collection.rb แต่พอเปลี่ยนชื่อแล้วก็กลับไปเจอปัญหาแรกอีก นั่นคือ NoMethodError

จากนั้นก็ลองย้ายไฟล์อีกที เพราะคิดว่า Rails อาจจะอยากให้มีไดเร็คทอรี่สำหรับ will_paginate ก็ได้ เลยย้ายไฟล์ไปที่ lib/will_paginate/collection.rb แทน หลังแก้ครั้งนี้ปรากฏว่าเจอปัญหาใหม่แทน:

NoMethodError: undefined method `create' for WillPaginate::Collection:Class
    from /opt.../mislav-will_paginate-2.3.8.../finder.rb:76:in `paginate'

ฮืมมม … จากอาการคร่าวๆ ดูแล้วน่าจะเกิดจากการที่เรา define class นี้ใน collection.rb ทำให้ Ruby ใช้ class นี้แทนสิ่งที่อยู่ใน will_paginate gem (เสมือนว่าเราสร้าง class ชื่อเดียวกันขึ้นมาเองอีกหนึ่ง class และ class ที่เราสร้างไม่มี method ต่างๆที่ will_paginate ต้องการ)

ปัญหาโลกแตกที่รอคอยการแก้ไขต่อไป…เนื่องจากดึกแล้วนั่นเอง

Written by sukita

เมษายน 1, 2009 at 11:50 pm

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

Tagged with