หัดเล่น I18n ใน Rails 2.2

(คัดลอกมาจาก http://www.rails66.com/blog/?p=604)

ฟีเจอร์ใหม่อีกอย่างหนึ่งของ Rails 2.2 ก็คือความสามารถในการทำ internationalization พื้นฐานที่ถูกบรรจุอยู่ใน core (อ่าน api, เว็บหลัก Rails I18n)

ก่อนจะใช้ได้ก็ต้องไปใช้ Rails 2.2 เสียก่อน ถ้าจะทดลองกับ project ใหม่ก็ติดตั้ง Rails 2.2 แล้วก็สั่งสร้าง project ได้เลย ถ้าจะแก้ project เก่าก็แก้ไฟล์ /config/environment.rb ตามนี้ครับ

# เปลี่ยน version rails
RAILS_GEM_VERSION = '2.2.2' unless defined? RAILS_GEM_VERSION

require File.join(File.dirname(__FILE__), 'boot')
  # ..
  # Setting locales
  config.i18n.default_locale = 'en'  # เปลี่ยนเป็น 'th' สำหรับไทย
  # ..
end

เปลี่ยนเสร็จก็อย่าลืมสั่ง rake rails:update ให้ปรับแก้อะไรต่าง ๆ ด้วยนะครับ

หลักการคร่าว ๆ ของการทำ i18n ใน rails ก็คือ เราจะแยกสตริงที่ใช้ไปใส่ไว้ในไฟล์ต่างหาก โดยแบ่งตามภาษา จากนั้นเวลาเราจะแสดงสตริงเหล่านั้นก็จะเรียกผ่านฟังก์ชัน I18n.translate หรือย่อ ๆ ว่า I18n.t แทนที่จะเขียนสตริงเหล่านั้นออกไปตรง ๆ

ที่เก็บของไฟล์สตริงเหล่านี้จะอยู่ใน /config/locales/ โดยสามารถเก็บเป็นไฟล์ yml หรือเป็น ruby hash ก็ได้ คราวนี้เราจะลองอะไรง่าย ๆ กันก่อน โดยลองไปเพิ่ม (หรือแก้) ไฟล์ en.yml ในไดเร็กทรอรีดังกล่าวเป็นดังด้านล่างครับ

en:
  hello: "Hello world"
  hello_name: "Hello, {{name}}"

  config:
    hello: "Hello, admin"

ทีนี้ เราไปทดลองเรียกสตริงดังกล่าวใน script/console ครับ

I18n.translate :hello                #=>; "Hello world"
I18n.t :hello                         #=>; "Hello world"
I18n.t :hello_name, :name => 'John'  #=>; "Hello, John"
I18n.t 'config.hello'                 #=>; "Hello, admin"

จากตัวอย่างด้านบน แสดงการเรียกสตริงแบบทั่วไป ส่วน hello_name เป็นสตริงที่รับพารามิเตอร์ name ส่วน config.hello เป็นการระบุสตริงแบบที่มีขอบเขต (อยู่ใน config)

ทีนี้ ลองไปสร้างไฟล์ th.yml แล้วใส่ข้อมูลตามด้านล่างนะครับ

th:
  hello: "สวัสดี"
  hello_name: "สวัสดี, {{name}}"

  config:
    hello: "กราบสวัสดีท่านผู้ดูแล"

แล้วไปทดลองใหม่ใน script/console ครับ

>> I18n.locale = 'th'                   # ตั้ง locale
>> I18n.translate :hello                #=>; "สวัสดี"
>> I18n.t :hello_name, :name => 'John'  #=>; "สวัสดี, John"
>> I18n.t 'config.hello'                #=>; "กราบสวัสดีท่านผู้ดูแล"

ทีนี้ ถ้าเราต้องการให้ locales เริ่มต้นของเราเป็น th เลย ก็ไปแก้บรรทัด config.i18n.default_locale ใน environment.rb นะครับ

เพื่อความสะดวกใน view ฟังก์ชัน I18n.t สามารถเรียกสั้น ๆ ได้ด้วย t ดังนั้นเราสามารถเขียน <%=t :hello %> ได้เลย

อันนี้เป็นการใช้งานแบบขั้นต้นนะครับ ถ้ามีเวลาจะมาเขียนเกี่ยวกับการแปลอื่น ๆ เช่นการแปลชื่อ model และ attributes ใน Active Record ต่อครับ ถ้าใครอยากเล่นก่อนก็ไปโหลดไฟล์ locale th.rb ที่มีการแปลข้อความใน Active Record และส่วนอื่น ๆ เช่นวันที่และจำนวนนับ (แปลโดยคุณ Sikachu! ขอบคุณมากครับ!) แล้วมาเล่นดูได้ครับ เอาไปใส่เพิ่มไว้ใน /config/locales แล้วก็เปลี่ยน locale เป็น th ดู

ไฟล์คำแปลดังกล่าวผมไปโหลดมาจาก github ซึ่งเป็นไฟล์ที่ Sven Fuchs เอามาจาก demo application (อ่านเพิ่ม) โดยการแปลในนั้นทำโดยคุณ Prem Sichanugrist (หรือคุณ Sikachu! นั่นเอง) อย่างไรก็ตาม ตอนผมเอามาลองแล้วพบว่าเหมือนการอ้างถึงสตริงใน active record มันจะเปลี่ยนไป ผมเลยแก้กลายมาเป็นไฟล์ด้านบนครับ (ส่ง patch ไปให้ Sven Fuchs แล้ว)

Dependency Injection กับ Ruby

(คัดลอกมาจาก http://www.rails66.com/blog/?p=593)

แนวคิดเกี่ยวกับ Dependency Injection เป็นแนวคิดที่สำคัญมากในการโปรแกรมสำหรับภาษาเช่น Java ด้วยสาเหตุหลาย ๆ ประการ

สาเหตุหนึ่งก็คือมันทำให้เราสามารถทำ unit test กับโปรแกรมที่มีการขึ้นต่อกันได้ ยกตัวอย่างเมท็อดด้านล่าง

public class RegistrationController {
	// ...
	void sendConfirmationEmail(User newUser) {
		MailSender sender = new MailSender();
		
		String msg = buildEmailMessage(newUser);
		sender.send(msg,myemail,newUser.getEmail());
	}
}

แทบเราจะไม่สามารถ test ได้เลยว่าเมท็อดดังกล่าวเรียก MailSender ได้ถูกต้องหรือเปล่า ที่ผมนึกออกคงจะต้องเข้าไปจัดการแก้โปรแกรมหลายจุดอยู่

ปัญหาก็มาจากการที่เมท็อดนี้สร้าง MailSender ขึ้นมาเอง ทำให้เราไม่สามารถเข้าไปแก้ไขได้ วิธีการที่นิยมใช้ในการจัดการเรื่องเหล่านี้ก็คือการแยกการขึ้นต่อกันของคลาส MailSender ออกมา โดยทำเป็นเมท็อดให้กำหนดค่าเข้าไป อาจจะที่ constructor หรือเขียนเป็นเมท็อดแยก หรือไม่ก็ใช้ DI framework ต่างๆ

เช่นแก้โปรแกรมเป็นแบบนี้

public class RegistrationController {
	// ...
	private MailSenderInterface mailSender;
	
	void setMailSender(MailSenderInterface sender) {
		mailSender = sender;
	}
	
	void sendConfirmationEmail(User newUser) {
		String msg = buildEmailMessage(newUser);
		mailSender.send(msg,myemail,newUser.getEmail());
	}
}

ในปัจจุบัน Java มี dependency injection framework หลายตัว (เท่าที่ผมทราบ) ซึ่งทำให้การ “ร้อย” (ของยืมพี่ป๊อกหน่อย) component ต่าง ๆ เข้าด้วยกันเป็นไปได้สะดวกมาก

แนวคิดดังกล่าวได้รับการตอบรับจากทางฝั่งนักพัฒนา Ruby เช่นเดียวกัน เช่น Jim Weirich ได้เขียนบล็อกเกี่ยวกับเรื่องนี้เอาไว้เมื่อปี 2004 ใน Ruby ก็มี framework ทำ DI อยู่หลายตัวเช่น Needle เขียนโดย Jamis Buck (คนทำ Capistrano) Jamis Buck ถึงขนาดเขียน di framework มาสองตัวเลยทีเดียว (อีกตัวคือ Copland)

อย่างไรก็ตาม ก็เป็นที่น่าสงสัยว่าทำไม DI framework ไม่เป็นที่นิยมใน Ruby

หนึ่งปีถัดมา Jim Weirich ได้ไปพูดที่ OSCON ในหัวข้อว่า “Dependency Injection: Vitally Important or Totally Irrelevant?” โดยสรุปว่าเนื่องจาก Ruby ไม่เหมือน Java ในปัจจุบันยังไม่เห็นความจำเป็นของ DI framework

Jamis Buck เองก็ออกมาเขียนถึงเรื่องดังกล่าวเช่นกัน โดยเขาแก้โปรแกรมในไลบรารี Net::SSH ใหม่ โดยเอา DI (ที่เขาเขียนเอง) ออก แล้วพบว่าโปรแกรมเล็กลงและอ่านง่ายขึ้น

ทำไม DI ดูเหมือนจะยังไม่จำเป็นใน Ruby?

พิจารณาจากตัวอย่างข้างต้น ถ้าเอาเมท็อด sendConfirmationEmail มาเขียนเป็นโปรแกรม Ruby จะได้ประมาณด้านล่างครับ

class RegistrationController
  # ..
  def send_confirmation_email(new_user)
    sender = MailSender.new
    msg = build_email_message(new_user)
    sender.send(msg, self.myemail, new_user.get_email)
  end
end

แล้วจะ test อย่างไร?

สิ่งที่เรามักจะลืมไปก็คือภาษาแต่ละภาษามีลักษณะที่แตกต่างกัน บางอย่างที่ไม่สามารถทำได้เลยในบางภาษา อาจเป็นสิ่งธรรมดามากในบางภาษา

ใน Ruby มีความสามารถ (หรือความบกพร่อง?) อย่างหนึ่งคือ Open Class

นั่นคือเราสามารถแกะคลาสมาแก้ได้ตลอดเวลา (รวมถึงตอน run-time) นอกจากนี้เรายังแก้ไขการทำงานของเมท็อดของแต่ละวัตถุได้โดยง่าย (ในระหว่างที่โปรแกรมทำงานอยู่เช่นกัน)

เมท็อดด้านบนถ้าจะเขียน test case ใน rspec ก็เป็นประมาณนี้ครับ

describe RegistrationController do
  #..
  it "should send mail to user's address from admin's mail" do
    my_email = 'jittat@internet.com'
    user_email = 'user@space.com'
    user = mock_model(User, :email =&gt; user_email)
    sender = mock(&quot;mock sender&quot;)
    sender.should_receive(:send).
      with(anything, my_email, user_email)
    MailSender.should_receive(:new).and_return(mock_sender)    
    controller = RegistrationController.new :adm_mail =&gt; my_email
    controller.send_confirmation_email(user)
  end
end

สังเกตว่าเนื่องจาก class และ object ใน ruby แก้ได้ตลอดเวลา mock framework จึงสามารถเข้าไปปรับแก้อะไรต่าง ๆ ได้มากมาย โดยไม่ต้องแยก dependency ออกมา

ไม่รู้ว่าผลที่ได้จะดีหรือไม่ดี? แต่ก็ทำให้ความจำเป็นของการใช้ DI framework ใน Ruby ลดลงไป

อ่านเพิ่มเติมได้ใน slide ของ Jim Weirich นะครับ อธิบายเห็นภาพมาก (มีอีกอันที่น่าสนใจเหมือนกันคือ 10 Things Every Java Programmer Should Know About Ruby ลองไปกดเล่นได้ครับ)

การใช้งาน Validation

(คัดลอกมาจาก http://www.rails66.com/blog/?p=496)

เวลาเราพัฒนาโปรแกรมประยุกต์ภายใต้กรอบงานแบบ MVC โดยเฉพาะบน Rails บางทีเราจะพบว่า model ของเรานั่นว่างโล่ง (เพราะว่าทำหน้าที่เชื่อมกับ table อย่างเดียว) ส่วน controller เราเต็มไปด้วยตรรกซับซ้อนซ่อนเงื่อน จนทำให้นึกไปถึงสมัยก่อนที่เขียนโปรแกรมแบบไม่มีโครงสร้างและใช้ goto กันจนโปรแกรมพันกันเป็นเส้นก๋วยเตี๋ยว

พักหลัง ๆ เลยมีคนพยายามบอกว่า model ควรจะอ้วน ๆ แต่ controller ควรจะผอมเพรียว (ตัวอย่างเพิ่มเติม)

แล้วอะไรบ้างที่สามารถนำไปอยู่ในโมเดลได้? ที่ชัดที่สุดก็คือการตรวจสอบความถูกต้องของข้อมูล (validation)

จริง ๆ แล้วในตัวอย่างและหนังสือ Rails แทบจะทุกเล่มทุกอันก็จะแสดงให้เห็นว่าการตรวจสอบความถูกต้องของข้อมูลทำได้ง่ายมากในโมเดล แต่บางทีเวลาเรารีบ ๆ เขียนก็มักจะลืม ๆ ไป หรือบางทีเราอาจจะมีการตรวจสอบอะไรที่แปลกไปจากรูปแบบการตรวจสอบพื้นฐานที่ Rails มีให้ เราก็เลยไปเขียนเอาไว้ใน controller เสียเลย

เราจะยกตัวอย่างจากโมเดล Book ที่มี attribute เป็น title และ page นะครับ

ลองดูโปรแกรมของ controller ด้านล่างที่รับค่าจาก form ผ่านทางเมท็อด create นะครับ

  def new
    @book = Book.new
  end

  def create
    @book = Book.new(params[:book])
    if @book.title==&quot;&quot;
      flash[:notice] = &quot;Error bad title&quot;
      render :action => 'new'
      return
    end
    if @book.pages==&quot;&quot;
      flash[:notice] = &quot;Error bad page number&quot;
      render :action => 'new'
      return
    end
    if @book.title[0]  ?Z
      flash[:notice] = &quot;Error title should begin with cap&quot;
      render :action => 'new'
      return
    end

    @book.save
    redirect_to :action => 'index'
  end

Controller ด้านบนมีการตรวจสอบสามอย่างคือ title ต้องไม่ว่าง, page ต้องไม่ว่าง, และ title ต้องขึ้นต้นด้วยตัวอักษรพิมพ์ใหญ่

เมื่อตรวจสอบผ่านแล้วก็จะเก็บข้อมูลไป ถ้ามีข้อผิดพลาดก็จะกลับไปแสดง view new กลับมาเหมือนเดิม เพื่อให้หน้า view แสดง flash[:notice] เพื่อบอกกับผู้ใช้ว่ามีข้อผิดพลาดทำให้เก็บค่าไม่ได้ และให้ผู้ใช้ป้อนค่าเข้ามาใหม่

ระบบการตรวจสอบข้อมูลของ Rails นั้นถูกออกแบบมาเพื่อให้ทำงานประสานกันตั้งแต่ใน model ไป controller ออกไป view ซึ่งเดียวเราจะได้ดูกันครับ

เราไปย้ายการตรวจสอบเข้าไปใน model กันครับ

ถ้าดู คู่มือ API ของ Rails จะพบว่าการตรวจสอบสองอย่างแรกเป็นการตรวจสอบมาตรฐานที่มีมาอยู่แล้ว ส่วนอันที่ 3 นั้นไม่มีทำให้ต้องเขียนเอง ดังนั้นผมขอสมมติว่าเราเลิกสนใจการตรวจสอบว่าชื่อหนังสือขึ้นด้วยตัวพิมพ์ใหญ่ไปก่อน

การตรวจสอบที่เกี่ยวข้องก็มี validates_presence_of (ตรวจสอบว่าไม่ว่าง นั่นคือไม่เป็น nil หรือ "") อีกอันก็คือ validates_numericality_of (ตรวจสอบว่าเป็นตัวเลข — สังเกตว่าเราตรวจได้ดีกว่าในตัวอย่าง controller ข้างต้น) เราก็ไปเพิ่ม validation ทั้งสองนี้ในโมเดลดังด้านล่างครับ

class Book < ActiveRecord::Base
  #...
  validates_presence_of :title
  validates_numericality_of :pages
  #...
end
&#91;/sourcecode&#93;

ส่วน validation ที่เราใส่ไปนี้จะทำงานเมื่อเราสั่ง <tt>save</tt> หรือเมื่อเราทดสอบว่าตรวจสอบผ่านหรือไม่ด้วยเมท็อด <tt>valid?</tt> (ซึ่งมักใช้ในกรณีที่ยังมีการทดสอบบางอย่างเหลืออยู่ที่ต้องเรียกเอง)  

ตัว controller ข้างต้นเราสามารถแก้ได้ดังนี้ครับ

  def create
    @book = Book.new(params[:book])
    if @book.save
      redirect_to :action => 'index'
    else
      render :action => 'new'
    end
  end

สังเกตว่าการตรวจสอบต่าง ๆ หายไปหมด (รวมถึงการจัดการพวกข้อความแสดงข้อผิดพลาดด้วย) ทีนี้ก่อนจะ save ข้อมูลในโมเดลของเราจะถูกตรวจสอบ ถ้าไม่ผ่านเมท็อด save จะคืนค่า false ทำให้เรากลับไปแสดงหน้า new ใหม่

ทีนี้ พวกข้อผิดพลาดต่าง ๆ ที่เกิดขึ้นระหว่างการตรวจสอบนั้น เราสามารถสั่งให้แสดงใน view ได้โดยเรียก error_messages_for สำหรับแสดงข้อผิดพลาดโดยรวมทั้งหมดของข้อมูลนั้น ๆ และ error_message_on ซึ่งจะแสดงข้อผิดพลาดเฉพาะ field ไป ตัวอย่างการใช้แสดงใน view new.html.erb ด้านล่าง

<h1>New Book</h1>

<%= error_messages_for :book %>

<% form_for :book,@book,:url =>{:action => 'create'} do |f| %>

  Title: <%= f.text_field :title %>   
  <%= error_message_on @book, 'title', 'The title ' %>
  <br/>
  Pages: <%= f.text_field :pages %>
  <%= error_message_on @book, 'pages', 'The number of pages ' %>
  <br/>
  <%= submit_tag %>
<% end %>

สังเกตการใช้งาน error_messages_for ที่อยู่ตรงหัว และ error_message_on ที่อยู่บริเวณ field ต่าง ๆ นะครับ ในส่วนของ error_message_on เรามีการแก้ให้แทนที่จะเรียก field ด้วยชื่อตรง ๆ ก็ให้เรียกเป็นภาษาที่ดูเป็นภาษาคนเสียหน่อยครับ ภาพด้านล่างแสดงตัวอย่างเวลาเกิด error ครับ (ปกติจะมีสีสรรค์สวยกว่านี้ครับ แต่ css ผมว่างเปล่าเลยดูไม่สวยเท่าใดครับ)

rails_ex-validation

ทีนี้ เหลือ validation อีกอันที่เราต้องการทำพิเศษ (custom) ก็ทำไม่ยากครับ วิธีการก็คือไปเขียนเมท็อดสำหรับตรวจสอบไว้ โดยเมท็อดนี้ถ้าพอข้อผิดพลาดก็ให้ใส่ข้อผิดพลาดนั้นลงใน attribute errors ด้วยเมท็อด add ครับ จากนั้นก็ไปบอก Active Record ให้ทำ validation ที่เราสร้างขึ้นมาโดยสั่ง validate ชื่อเมท็อด ทีตรงหัวโมเดล ตัวอย่างในด้านล่างครับ

class Book < ActiveRecord::Base
  #...
  validate :title_begins_with_capital_letters     # บอกให้ตรวจด้วย

  #...
  protected

  def title_begins_with_capital_letters
    return if self.title==nil
    return if self.title.length==0

    if self.title&#91;0&#93; < ?A or self.title&#91;0&#93; > ?Z
      # เพิ่ม errors
      errors.add &quot;title&quot;,&quot;must begin with a capital letter&quot;  
    end
  end
end

สังเกตว่าค่าที่คืนจากเมท็อดนี้จะไม่ถูกนำไปใช้ สิ่งที่สนใจคือ attribute errors อย่างเดียวเท่านั้น นอกจากการประกาศข้อผิดพลาดที่เกิดกับ field แล้ว เรายังเพิ่มข้อผิดพลาดให้แสดงในภาพรวมได้ ด้วยเมท็อด add_to_base ได้

โดยสรุปก็คือระบบ validation ของ Rails จะเริ่มที่การประกาศไว้ใน model (ด้วย validates_*) เรียกตรวจสอบใน controller (ด้วย save หรือ valid?) แล้วไปแสดงผลที่ใน view (ด้วย error_messages_for กับ error_message_on)

การใช้ partial และ helper

(คัดลอกจาก http://www.rails66.com/blog/?p=424)

Rails มีวิธีที่เราสามารถใช้เพื่อทำให้ view ของเราอ่านและจัดการได้ง่ายอยู่หลายวิธี

เราจะแสดงตัวอย่างโดยค่อย ๆ แก้ view ของเราด้านล่างนี้ที่ใช้แสดงข้อมูลของหนังสือกับผู้เขียนครับ

<ul>
  <% @books.each do |book| %>
    <li>
      <b><%= book.title %></b><br/>
      Written by:
      <%= (book.authors.collect do |a| 
             &quot;#{a.first_name} #{a.last_name}&quot;
           end).join(&quot;,&quot;) %>
    </li>
  <% end %>
<ul>

หน้าตาของ view เมื่อแสดงผลแล้วเป็นดังด้านล่างครับ

rails_partial

view นี้รับรายการ @books ที่ค้นมาจาก controller วัตถุ Book จะมีความสัมพันธ์ไปยังโมเดล Writer ผ่านทาง collection authors โดยวัตถุคลาส Writer จะมี first_name กับ last_name

view ด้านบนอาจจะดูสั้น ๆ แล้วไม่ซับซ้อนมากนัก แต่ว่ามีสิ่งที่เราทำให้ดีขึ้นได้หลายอย่าง

อย่างแรกคือใน view นี้มีโปรแกรม Ruby ที่ใช้จัด format รายการผู้เขียนเพื่อแสดงผล (บรรทัด 6-8) กล่าวคือในรายการผู้เขียน ถ้ามีหลายคนเราต้องการคั่นระหว่างชื่อด้วยเครื่องหมายลูกน้ำ (“,”) ใน view ข้างต้นก็เลยมีการเรียก book.authors.collect เพื่อเอาชื่อ (first_name) กับนามสกุล (last_name) มาต่อกัน แล้วมาเรียก join อีกที

เราจะย้ายส่วนของโปรแกรมนี้ออกไปนอก view ครับ โดยที่ที่เหมาะสมสำหรับมันก็คือใน helper (โดยปกติสำหรับ controller หนึ่ง ๆ จะมีโมดูล helper หนึ่งโมดูล โดย Rails จะสร้าง /app/helpers/ชื่อ_helper.rb ไว้ให้ครับ) ในที่นี้เราจะใส่ไว้ที่ books_helper.rb ดังด้านล่างครับ

module BooksHelper
  def format_authors(authors)
    authornames = authors.collect do |a| 
      &quot;#{a.first_name} #{a.last_name}&quot;
    end
    authornames.join(&quot;, &quot;)
  end
end

แล้วเราก็ไปตัดส่วนดังกล่าวออกจาก view ครับ ได้ผลดังด้านล่าง

<ul>
  <% @books.each do |book| %>
    <li>
      <b><%= book.title %></b><br/>
      Written by: <%= format_authors(book.authors) %>
    </li>
  <% end %>
<ul>

ทีนี้สังเกตว่า ถ้าในการแสดงผลหนังสือแต่ละเล่มค่อนข้างซับซ้อน จะทำให้ view นี้อ่านยาก (แต่ในกรณีของเราค่อนข้างอ่านง่ายแล้ว) เราสามารถย้ายส่วนดังกล่าวออกไปใส่ไว้ใน view เล็ก ๆ ที่ Rails เรียกว่า partial ได้ โดยเราจะสร้าง partial ชื่อ book โดยเก็บไว้ในแฟ้มชื่อ _book.html.erb ในไดเร็กทอรีเดียวกับ view เดิม

สังเกตว่า partial ชื่อ book แต่ชื่อไฟล์จะขึ้นต้นด้วยขีดล่าง เป็น _book.html.erb นะครับ ใน partial ดังกล่าวก็ตัดส่วนที่แสดงผล book ออกมาเลย ดังด้านล่าง

  <b><%= book.title %></b><br/>
  Written by: <%= format_authors(book.authors) %>

โดยปกติแล้ว partial จะได้รับตัวแปรภายในมาจาก view ด้วย (เช่นพวก @books) ในกรณีนี้ เราใช้ตัวแปร book ที่อ้างมาจากในวนรอบข้างต้นครับ ดังนั้นเราจะต้องส่งตัวแปรพวกนี้ให้กับ partial เอง ใน view หลัก เราจะเรียก render :partial โดยส่งค่าตัวแปร book ไปด้วย ผ่านทาง option locals ดังด้านล่างครับ

<ul>
  <% @books.each do |book| %>
    <li>
      <%= render :partial => 'book', 
                  :locals => {:book => book} %>
    </li>
  <% end %>
<ul>

นอกจากที่เราจะใช้ partial เพื่อแสดงผลบางส่วนของ view แล้ว คำสั่ง render :partial ยังมีอีก option หนึ่งที่มีประโยชน์มาก ก็คือการสั่งให้แสดงให้ทุก ๆ ข้อมูลในรายการ

จากตัวอย่างข้างต้น view หลักเราสามารถเขียนใหม่เหลือแค่

<ul>
  <%= render :partial => 'book', :collection => @books %>
<ul>

สังเกตว่าเราไม่ต้องมานั่ง for อีกต่อไปแล้ว

การทำงานของ render collection จะวิ่งเข้าไปในรายการแล้วเรียก render ข้อมูลแต่ละตัว เนื่องจากเราต้องการครอบทุก ๆ ข้อมูลด้วยแท็ก <li> เราจึงต้องย้ายไปไว้ใน partial _book.html.erb ดังด้านล่างครับ

 <li>
   <b><%= book.title %></b><br/>
   Written by: <%= format_authors(book.authors) %>
 </li>

ในการเรียกใช้ render collection เราใช้ข้อตกลงอย่างหนึ่งครับ นั่นคือข้อมูลแต่ละตัวจาก collection จะถูกส่งไปยัง partial ด้วยชื่อตัวแปรที่เป็นชื่อของ partial ครับ เช่น ในกรณีนี้คือตัวแปร book ครับ

ขอแถมนิดหนึ่งครับ ถ้าเขียน partial ด้วย haml ผลที่ได้จะยิ่งโล่งโปร่งสบายเข้าไปอีก ดังด้านล่างครับ

%li
  %b= book.title
  %br/
  Written by:
  = format_authors(book.authors)

สนุกกับ Active Record: ใช้ Active Record และโมเดลนอก Rails

(คัดลอกจาก http://www.rails66.com/blog/?p=412)

Active Record ก็เป็นชุดไลบรารีที่สามารถใช้งานได้นอกเหนือจากภายในโปรแกรมประยุกต์บน Rails ครับ

ทีนี้ มีการใช้งานหลัก ๆ สองแบบ คือแบบที่เราอ้างถึงฐานข้อมูลโดยไม่เกี่ยวข้องกับโมเดลใน Rails เลย กับแบบที่ต้องการใช้โมเดลที่สร้างไว้ใน Rails ด้วย

เอาแบบแรกก่อนครับ

ในการใช้ก็ต้องโหลดตัวไลบรารีมาก่อน

require 'rubygems'
require 'active_record'

จากนั้นก็สั่งเปิด connection เองเลยครับ เช่น

ActiveRecord::Base.establish_connection({
      :adapter => "sqlite3", 
      # ใส่ชื่อไฟล์ฐานข้อมูล
      :database => "/home/test-ar/db/development.sqlite3"   
})

ในกรณีที่ใช้ sqlite3 หรือเช่นด้านล่าง ในกรณีที่ใช้ mysql

ActiveRecord::Base.establish_connection(
  :adapter  => "mysql",
  :host     => "localhost",
  :username => "me",
  :password => "secret",
  :database => "activerecord"
)

พอสร้าง connection เสร็จ ก็สร้างคลาสลูกหลานของ ActiveRecord::Base แล้วก็เรียกมาใช้ได้เลยครับ เช่น

class Book < ActiveRecord::Base
end

Book.find(:all).each do |book|
  puts "#{book.title}"
end
&#91;/sourcecode&#93;

เราสามารถใช้การเชื่อมโยงต่าง ๆ เช่น <tt>has_many</tt>, <tt>belongs_to</tt> ได้เหมือนปกติครับ

ทีนี้ ถ้าเราต้องการใช้โมเดลจาก Rails ด้วย เราจะต้องโหลดโมเดลและอื่น ๆ มาด้วย หลัก ๆ ก็คือไปเรียก <tt>config/environment</tt> มาครับ นอกจากนี้ ถ้าเราต้องการเลือก environment ที่จะทำงาน ว่าจะเป็น development, production หรือ test ก็สามารถทำได้ โดยกำหนดค่าลงไปที่ <tt>ENV["RAILS_ENV"]</tt> ครับ เช่นสั่ง


# กำหนด environment ถ้าไม่กำหนดจะเป็น development โดยอัตโนมัติ
ENV["RAILS_ENV"] = "development"
RAILS_PATH = '/home/jittat/prog/rails/test-ar'
require File.join(RAILS_PATH, "config/environment")

ต้องระวังนิดหน่อยถ้าเราต้องการเรียกใช้ข้อมูลในโมเดล พร้อม ๆ กับที่โปรแกรมบนเว็บของเราทำงาน ถ้ามีการแก้ไขข้อมูลพร้อมกันจะอย่าลืมจัดการเรื่องการ lock ตารางด้วยครับ (ดูเพิ่มได้ที่ ActiveRecord::Locking)

เอกสารอ้างอิง/ลิงก์เพิ่มเติม

สนุกกับ Active Record: การเชื่อมโยงแบบ many-to-many (2)

(คัดลอกจาก http://www.rails66.com/blog/?p=381)

จากตอนที่แล้วเราได้ดูการจัดการการเชื่อมโยงแบบ many-to-many โดยใช้ has_and_belongs_to_many ไปแล้ว ในตอนนี้เราจะพิจารณาอีกวิธีหนึ่ง ซึ่งดูแล้วใช้ง่ายกว่า (แล้วทำไมไม่เขียนในตอนแรกนะ?)

เราจะพิจารณาตัวอย่างเพิ่มเติม โดยเพิ่มโมเดลผู้อ่าน (Reader) เข้าไป จากนั้นเราจะสร้างความสัมพันธ์แบบ many-to-many ระหว่างวัตถุในโมเดล Reader กับโมเดล Book ด้วยโมเดล Reading ที่นอกจากจะเชื่อมวัตถุในโมเดลทั้งสองเข้าด้วยกันแบบ many-to-many แล้วยังเก็บข้อมูลลงไปในความสัมพันธ์ด้วยว่าผู้อ่านนั้นอ่านหนังสือเล่มนั้นเมื่อใด

หลักการคร่าว ๆ ก็คือวัตถุในโมเดล Reading จะเป็นสมาชิกของทั้งโมเดล Reader และโมเดล Book ทำให้วัตถุในโมเดลทั้งสองสามารถ has_many Reading ได้พร้อม ๆ กัน จากตรงนี้นี่เองที่ทำให้เกิดความสัมพันธ์แบบ many-to-many ได้

อย่างไรก็ตาม ถ้าทำแค่นั้นการอ้างถึง Reader จาก Book จะต้องอ้างผ่านทางวัตถุของโมเดล Reading ใน Active Record ได้ทุ่นแรงเราด้วย option :through ที่จะทำให้การอ้างผ่านของเราเป็นไปได้อย่างอัตโนมัติ

ท่าที่เราจะทำนี้ ตอนต้นจะคล้าย ๆ กับท่าแรกในบทความ การจัดการ GORM Relationship แบบ Many-to-Many ที่ grails66 ครับ แต่ตรงส่วน :through จะทำให้การอ้างถึงง่ายขึ้น

เริ่มเลยแล้วกันครับ

ไปสร้างโมเดล Reader กันก่อน ให้มี field name เป็นสตริง

จากนั้นสร้างโมเดล Reading ด้วย migration ด้านล่าง สังเกตว่านอกจาก foreign key ทั้งสองแล้ว เรายังมี field read_at ไว้ด้วย

class CreateReadings < ActiveRecord::Migration
  def self.up
    create_table :readings do |t|
      t.column "book_id", :integer
      t.column "reader_id", :integer
      t.column "read_at", :datetime
      t.timestamps
    end
  end
  # ... ละไว้ ...
end
&#91;/sourcecode&#93;

สังเกตว่าด้วยโมเดล <tt>Reading</tt> ที่อยู่ในทั้งสองโมเดล เราสามารถใช้ <tt>belongs_to</tt> กับ <tt>has_many</tt> เชื่อมวัตถุในโมเดลนี้เข้ากับอีกสองโมเดลได้

เราไปแก้ <tt>book.rb</tt>, <tt>reader.rb</tt>, และ <tt>reading.rb</tt> โดยใส่ความสัมพันธ์ดังกล่าวลงไปครับ 


class Book < ActiveRecord::Base
  #.. ละไว้ ..
  has_many :readings                            
end
&#91;/sourcecode&#93;

&#91;sourcecode lang="ruby"&#93;
class Reader < ActiveRecord::Base
  has_many :readings
end
&#91;/sourcecode&#93;

&#91;sourcecode lang="ruby"&#93;
class Reading < ActiveRecord::Base
  belongs_to :reader
  belongs_to :book
end
&#91;/sourcecode&#93;

แค่นี้ก็พอทดลองได้แล้วครับ

<pre>
# สร้างหนังสือ Rails มาอีกสักเล่ม
&gt;&gt; <b>ror = Book.create(:title =&gt; "Ruby on Rails", :pages =&gt; 400)</b>
=&gt; #&lt;Book id: 5, title: "Ruby on Rails", pages: 400, published_at: nil,.. &gt;

# สร้างคนอ่านอีกสองคน (รักคนอ่าน อิอิ)
&gt;&gt; <b>black = Reader.create :name =&gt; "Black"</b>
=&gt; #&lt;Reader id: 2, name: "Black",..&gt;
&gt;&gt; <b>pink = Reader.create :name =&gt; "Pink"</b>
=&gt; #&lt;Reader id: 3, name: "Pink",..&gt;

# จับสองคนนี้ให้อ่าน RoR โดยสร้างวัตถุของโมเดล Reading
&gt;&gt; <b>Reading.create(:reader =&gt; pink, :book =&gt; ror, :read_at =&gt; Time.now)</b>
&gt;&gt; <b>Reading.create(:reader =&gt; black, :book =&gt; ror, :read_at =&gt; Time.now-1.day)</b>

# ทดลองดูที่ RoR
&gt;&gt; <b>ror.readings</b>
=&gt; [#&lt;Reading id: 1, book_id: 5, reader_id: 3, 
read_at: "2008-09-29 02:45:30", ..&gt;, #&lt;Reading id: 2, book_id: 5, 
reader_id: 2, read_at: "2008-09-28 02:45:51",..&gt;]

# เอาคนอ่านออกมา โดยเรียกผ่านทาง readings
# เมท็อด collect จะวิ่งไล่ไปในรายการ แล้วก็ทำงานของในรายการ
#   ตาม block ที่ระบุไว้แล้วคือรายการของผลลัพธ์ออกมา
#   ในที่นี้ block  { |r| r.reader } ระบุว่าให้รับของมาใส่ในตัวแปร r
#   แล้วเรียกเมท็อด reader
&gt;&gt; <b>ror.readings.collect {|r| r.reader }</b>
=&gt; [#&lt;Reader id: 3, name: "Pink", ..&gt;, 
#&lt;Reader id: 2, name: "Black", ..&gt;]

# ทดลองกับหนังสือบ้าง
&gt;&gt; <b>algo = Book.find_by_title "Algorithms"</b>
=&gt; #&lt;Book id: 4, title: "Algorithms", pages: 1000, ..&gt;

# ให้ Mr.Pink อ่าน algo เพิ่มด้วย
&gt;&gt; <b>Reading.create :reader =&gt; pink, :book =&gt; algo, :read_at =&gt; Time.now - 1.year</b>

# ดูรายการของ Mr.Pink
&gt;&gt; <b>pink.readings</b>
=&gt; [#&lt;Reading id: 1, book_id: 5, reader_id: 3, ..&gt;, 
#&lt;Reading id: 3, book_id: 4, reader_id: 3, ..&gt;]
</pre>

แค่นี้ก็ทำ many-to-many โดยผ่านอีกโมเดลหนึ่งได้แล้ว

อย่างไรก็ตาม ที่น่าสนใจคือตรงขั้น

>> ror.readings.collect {|r| r.reader }

ใน Active Record มี option พิเศษใน has_many ที่ทำให้เราระบุการเชื่อมต่อ ผ่าน อีกโมเดลในลักษณะข้างต้นได้อย่างสะดวก นั่นคือ option :through

เราไปปรับโมเดล Reader และ Book โดยเพิ่ม has_many :through เข้าไปครับ

class Book < ActiveRecord::Base
  # .. ละไว้ ..
  has_many :readers, :through => :readings
  has_many :readings
end
class Reader < ActiveRecord::Base
  has_many :readings
  has_many :books, :through => :readings
end

ตัว option :through => :readings ที่ใส่ไประบุว่าความสัมพันธ์ readers หรือ books เนี่ยะ ให้ไปวิ่งผ่านความสัมพันธ์ readings

แก้แล้วก็เข้า script/console ใหม่ เพื่อทดลองอีกสักหน่อย

>> pink = Reader.find_by_name "Pink"
=> #<Reader id: 3, name: "Pink", ..>

>> pink.books
=> [#<Book id: 5, title: "Ruby on Rails", ..>, 
#<Book id: 4, title: "Algorithms", ..>]

ทดลองลบสักหน่อยครับ

>> ror = pink.books[0]
=> #<Book id: 5, title: "Ruby on Rails", ..>

>> pink.books.delete(ror)

# หายไปแล้ว
>> pink.books
=> [#<Book id: 4, title: "Algorithms", ..>]
>> ror.readers
=> [#<Reader id: 2, name: "Black", ..>]

หมายเหตุเพิ่มเติมเล็กน้อย สังเกตว่าครั้งก่อน ๆ บางทีที่เราเรียก associateion แล้วเราต้องใส่อาร์กิวเมนต์ true เข้าไป เพื่อให้มัน reload ใหม่ แต่ที่ทดลองครั้งนี้ไม่ได้ใส่เลย เพราะว่าเวลาเราเรียก ror.readers เช่นด้านบน ความสัมพันธ์ readers ยังไม่ถูกอ่านมาก่อน เมื่อตอนเราต้น ror มา แต่จะถูกอ่านเมื่อเราเรียกใช้ เราสามารถใส่ option :include ตอนสั่ง find ได้เพื่อกำหนดให้ Active Record อ่านเอาความสัมพันธ์นี้มาเลยตั้งแต่ตอนค้นวัตถุ

สมมติว่ามี Reader คนหนึ่งอ่าน ror ซ้ำหลายรอบ เวลาเรียก ror.readers เราจะได้ Reader นั้นออกมาซ้ำ ๆ กันด้วย ถ้าต้องการแค่ครั้งเดียว เราสามารถเพิ่ม option :uniq => true เข้าไปตอนนิยาม has_many ได้ดังด้านล่างครับ

class Book < ActiveRecord::Base
  # .. ละไว้ ..
  has_many :readers, :through => :readings, :uniq => true
  has_many :readings
end

สนุกกับ Active Record: การเชื่อมโยงแบบ many-to-many (1)

(คัดลอกมาจาก http://www.rails66.com/blog/?p=359)

คราวก่อนโน้น เราได้ทดลองสร้างความสัมพันธ์ระหว่างโมเดล Writer กับโมเดล Book เอาไว้ โดยเป็นความสัมพันธ์แบบ one-to-many โดยให้ตารางของโมเดล Book เก็บ foreign key เป็น id ของวัตถุในโมเดล Writer เอาไว้

แต่จริง ๆ หนังสือเล่มหนึ่งอาจมีนักเขียนได้หลายคน และนักเขียนคนหนึ่งก็อาจจะเขียนหนังสือหลายเล่ม ทำให้ความสัมพันธ์ระหว่างโมเดลทั้งสองนี้ กลายเป็นความสัมพันธ์แบบ many-to-many ไป

เนื่องจากข้อจำกัดของฐานข้อมูลแบบ relational ที่ว่าหนึ่งแถวในแต่ละฟิลด์จะเก็บข้อมูลได้แค่ตัวเดียว การจะใส่ foreign key ของ book_id เข้าไปในตาราง writers คงจะไม่ทำให้เราสามารถสร้างความสัมพันธ์ดังกล่าวได้

ดังนั้นวิธีที่เราทำได้ใน “ระดับฐานข้อมูล” คือการสร้างตารางขึ้นมาอีกตารางหนึ่ง เพื่อเชื่อมโยงวัตถุทั้งสองโมเดลเข้าด้วยกัน ตารางดังกล่าวเรียกว่า join table แสดงรูปได้ดังด้านล่าง

                             ------------            --------------
  -------------                JoinTable                  Book
      Writer                 ============            ==============
  =============              - book_id ------------> - id
  - id         <------------ - writer_id             - title
  - first_name               ------------            - pages
  - last_name                                        - published_at
  -------------                                      --------------

สังเกตว่าตารางดังกล่าวจะเชื่อมโยงวัตถุสองวัตถุจากสองโมเดลเข้าด้วยกันครับ ถ้าสองวัตถุใดเชื่อมกัน เราก็จะมีแถวในตาราง join table นี้ที่ชี้ไปหาทั้งสองวัตถุนั้น

ทีนี้เราจะทำใน Active Record อย่างไร?

มีสองวิธีหลัก ๆ ขึ้นกับว่าเราต้องการให้ join table นี้เป็นโมเดลในระบบของเราด้วยหรือไม่ เช่นในกรณีที่ความสัมพันธ์ระหว่างวัตถุจากสองโมเดลมีข้อมูลอื่น ๆ ที่ต้องการเก็บด้วย เพื่อที่จะอ้างถึงข้อมูลเพิ่มเติมเหล่านี้ เราก็จะต้องสร้างโมเดลขึ้นมาครอบตารางนั้นไว้

สำหรับตอนแรกนี้ เราจะสนใจเฉพาะการเชื่อมสองโมเดลเข้าด้วยกันเท่านั้นก่อน ดังนั้นตาราง join table ที่เราสร้างขึ้นจะทำให้การเชื่อมโยงเกิดขึ้นได้แต่จะมองไม่เห็นจาก Active Record

วิธีนี้จะระบุการเชื่อมโยงด้วย has_and_belongs_to_many (ยาวมาก นิยมเขียนย่อว่า habtm) ซึ่งค่อนข้างยุ่งสักหน่อย ไว้ตอนหน้าเราจะดูอีกวิธีในกรณีที่ join table ของเราเป็นโมเดลด้วย ซึ่งจะทำให้ได้การเชื่อมโยงที่เข้าใจได้ง่ายกว่ามาก

ในการสร้าง join table เรามีข้อตกลงว่าชื่อของตารางจะต้องเป็นชื่อของตารางของโมเดลทั้งสองต่อกัน (คั่นด้วยขีดล่าง “_”) โดยเรียงชื่อตารางตามตัวอักษร (เช่นเคย, ข้อตกลงนี้ปรับแต่งได้ครับ)

ดังนั้นในกรณีของเรา join table จะชื่อ books_writers เราไปสร้างตารางดังกล่าวด้วย migration ครับ

class CreateJoinTableBooksWriters < ActiveRecord::Migration
  def self.up
    create_table "books_writers", :id => false do |t|
      t.column "book_id", :integer
      t.column "writer_id", :integer
    end
  end

  def self.down
    drop_table "books_writers"
  end
end

แล้วก็ เรียก migration ให้ทำงานด้วย rake db:migrate

ทีนี้ เนื่องจากในการทดลองก่อน ๆ เราได้ไปสร้าง writer_id ไว้ในตาราง books เดี๋ยวเราต้องไปลบออกด้วย เพราะว่าไม่ได้ใช้แล้ว ก็ไปสร้าง migration สำหรับลบคอลัมน์นั้นครับ แน่นอนว่าความสัมพันธ์ที่เราเคยสร้างมาก็คงจะหายไปหมดแล้ว (หมายเหตุ: เราสามารถเขียนใน migration ให้คัดลอกความสัมพันธ์เดิมมาได้ แต่ขอละไว้ครับ)

class RemoveWriterIdFromBooks < ActiveRecord::Migration
  def self.up
    remove_column "books", "writer_id"
  end

  def self.down
    raise ActiveRecord::IrreversibleMigration
  end
end
&#91;/sourcecode&#93;

สังเกตว่าใน migration นี้ตรง <tt>self.down</tt> เราสั่งให้เกิด exception <tt>ActiveRecord::IrreversibleMigration</tt> เพราะว่า migration นี้ย้อนกลับไม่ได้ (ถ้าต้องการให้ย้อนได้เราต้องแก้ข้อมูลกลับคืน ซึ่งในกรณีนี้ทำไม่ได้ เนื่องจากในการเปลี่ยนตารางกลับจาก many-to-many ไปเป็น one-to-many อาจมีข้อมูลสูญหาย)

เมื่อจัดการกับ migration เสร็จแล้วก็เรียกให้ทำงาน แล้วเราจะไปแก้ไขโมเดลกัน

ในโมเดล เราก็ไปประกาศความสัมพันธ์ด้วย <tt>has_and_belongs_to_many</tt>  (อย่าลืมลบความสัมพันธ์เก่า พวก <tt>has_many</tt>, <tt>belongs_to</tt> ออกด้วยนะครับ)  ได้ดังด้านล่างครับ

ที่โมเดล <tt>Book</tt>

class Book < ActiveRecord::Base
  has_and_belongs_to_many :writers
end
&#91;/sourcecode&#93;

ที่โมเดล <tt>Writer</tt>

class Writer < ActiveRecord::Base
  has_and_belongs_to_many :books
  #... ละด้านล่างไว้..
end
&#91;/sourcecode&#93;

ไปทดลองกันใน console ครับ

<pre>
&gt;&gt; <b>Writer.find(:all)</b>    # เอาคนเขียนมาดูกันก่อน
=&gt; [#&lt;Writer id: 1, .., first_name: "John", last_name: "Madman"&gt;, 
#&lt;Writer id: 2, .., first_name: "John", last_name: "Bestman"&gt;]
&gt;&gt; <b>john = Writer.find(1)</b>  # ขอ john คนแรก
=&gt; #&lt;Writer id: 1, .., first_name: "John", last_name: "Madman"&gt;

&gt;&gt; <b>Book.find(:all)</b>   # เอาหนังสือมาดูกัน
=&gt; [#&lt;Book id: 3, title: "AJAX Tricks", pages: 300, ..&gt;, 
#&lt;Book id: 4, title: "Algorithms", pages: 1000, ..&gt;]

# เลือกหนังสือมาสองเล่ม
&gt;&gt; <b>algo_book = Book.find_by_title 'Algorithms'</b>     # =&gt; #&lt;Book id: 4, title: "Algorithms", ..&gt;
&gt;&gt; <b>ajax_book = Book.find(3)</b>     #  =&gt; #&lt;Book id: 3, title: "AJAX Tricks", ..&gt;

# ใส่ให้ john
&gt;&gt; <b>john.books &lt;&lt; algo_book</b>
&gt;&gt; <b>john.books &lt;&lt; ajax_book</b>

# ดูหนังสือที่ john เขียน
&gt;&gt; <b>john.books</b>
=&gt; [#&lt;Book id: 4, title: "Algorithms", pages: 1000, ..&gt;, 
#&lt;Book id: 3, title: "AJAX Tricks", pages: 300, ..&gt;]

# สร้างนักเขียน mary (กลับมาใหม่ หลังจากลบไปคราวที่แล้ว)
&gt;&gt; <b>mary = Writer.create(:first_name =&gt; 'Mary', :last_name =&gt; 'Happy')</b>
=&gt; #&lt;Writer id: 4, .., first_name: "Mary", last_name: "Happy"&gt;

# ให้ mary เขียน ajax ด้วย
&gt;&gt; <b>ajax_book.writers &lt;&lt; mary</b>
=&gt; [#&lt;Writer id: 4,..., first_name: "Mary", last_name: "Happy"&gt;]

# ดูว่าใครเขียน ajax บ้าง (ทั้งหมด) เราใส่ true ไปเพื่อให้ reload ไม่เช่นนั้นจะเห็นแค่ mary, ไม่เห็น john
&gt;&gt; <b>ajax_book.writers(true)</b>
=&gt; [#&lt;Writer id: 1, .., first_name: "John", last_name: "Madman"&gt;, 
#&lt;Writer id: 4, .., first_name: "Mary", last_name: "Happy"&gt;]
</pre>

ทีนี้ถ้าจะลบของออกจากความสัมพันธ์ เราสามารถใช้ <tt>delete</tt> ที่ collection ได้เลย
<pre>
&gt;&gt; <b>john.books.delete(algo_book)</b>   # ลบ algo ออกจากรายชื่อหนังสือของ john
&gt;&gt; <b>john.books</b>
=&gt; [#&lt;Book id: 3, title: "AJAX Tricks", ..&gt;]

&gt;&gt; <b>Book.find(:all)</b>    # แต่หนังสือยังอยู่ครบ
=&gt; [#&lt;Book id: 3, title: "AJAX Tricks", pages: 300, ..&gt;, 
#&lt;Book id: 4, title: "Algorithms", pages: 1000, ..&gt;]

&gt;&gt; <b>algo_book.writers(true)</b>    # เหลือ mary เขียนคนเดียว
=&gt; [#&lt;Writer id: 4, .., first_name: "Mary", last_name: "Happy"&gt;]
</pre>

นอกจากนี้เรายังค้นหาของที่อยู่ในความสัมพันธ์ได้ด้วย เช่นด้านล่าง
<pre>
# หาเฉพาะหนังสือที่ mary เขียน ที่หนากว่า 400 หน้า
&gt;&gt; <b>mary.books.find(:all, :conditions =&gt; 'pages &gt; 400')</b>   
=&gt; [#&lt;Book id: 4, title: "Algorithms", pages: 1000, ..&gt;]
</pre>

ทีนี้ ถ้าเราต้องการเรียกคนเขียนว่า authors แทน ก็ทำได้โดยระบุ habtm ในคลาส <tt>Book</tt> ใหม่ดังด้านล่างครับ

class Book < ActiveRecord::Base
  has_and_belongs_to_many :authors,
                          :class_name => 'Writer'                            
end

คราวหน้ากลับมาดูการทำ many-to-many กันอีกวิธีครับ ซึ่งจะเป็นการใช้ options :through ของ has_many แทน