หัดเล่น 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

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

ตัว 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[0] < ?A or self.title[0] > ?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

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

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

# กำหนด 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

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

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

class Book < ActiveRecord::Base
  #.. ละไว้ ..
  has_many :readings                            
end

class Reader < ActiveRecord::Base
  has_many :readings
end

class Reading < ActiveRecord::Base
  belongs_to :reader
  belongs_to :book
end

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

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

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

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

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

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

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

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

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

แค่นี้ก็ทำ 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

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

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

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

ที่โมเดล Book

class Book < ActiveRecord::Base
  has_and_belongs_to_many :writers
end

ที่โมเดล Writer

class Writer < ActiveRecord::Base
  has_and_belongs_to_many :books
  #... ละด้านล่างไว้..
end

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

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

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

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

# ใส่ให้ john
>> john.books << algo_book
>> john.books << ajax_book

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

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

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

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

ทีนี้ถ้าจะลบของออกจากความสัมพันธ์ เราสามารถใช้ delete ที่ collection ได้เลย

>> john.books.delete(algo_book)   # ลบ algo ออกจากรายชื่อหนังสือของ john
>> john.books
=> [#<Book id: 3, title: "AJAX Tricks", ..>]

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

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

นอกจากนี้เรายังค้นหาของที่อยู่ในความสัมพันธ์ได้ด้วย เช่นด้านล่าง

# หาเฉพาะหนังสือที่ mary เขียน ที่หนากว่า 400 หน้า
>> mary.books.find(:all, :conditions => 'pages > 400')
=> [#<Book id: 4, title: "Algorithms", pages: 1000, ..>]

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

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

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

ทำปลอม conversation context (เท่าที่ทำเป็น)

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

เห็น hangman ใน seam แล้วหนาวหลายรอบ (แถมวันนี้เพิ่งจะแสดงความเป็น component ไปอีก)

อย่างแรกที่น่าสนใจก็คือเรื่องของ conversation context ที่เท่าที่ผมพอจะเข้าใจก็คือคล้ายกับ session แต่ว่ามีอายุสั้นกว่า และในหนึ่ง browser มีได้หลายอัน (ต่าง tab ก็ต่าง context กันได้) แถมเท่าที่ดู ๆ ก็อาจจะทำอะไรได้อีกมากมาย

คิดว่าใน Rails ไม่ได้รองรับอะไรอย่างนี้โดยตรง ก็เลยอยากจะทดลองทำดู ก็ทำเฉพาะที่ผมเข้าใจก่อนแล้วกันครับ ก็คือทำอย่างไรให้มี context ที่สร้างง่าย ใช้ง่าย แล้วก็แต่ละ tab นั้นเป็นอิสระต่อกัน

เห็น comment ใน seam66 ของคุณ wiennat ถามคุณ deans4j ว่ามันเป็นการใช้ hidden input, cookies, และ session หรือเปล่า? ผมก็เลยคิดว่าเดี๋ยวทดลองทำดูโดยใช้ hidden input แล้วกัน (เพราะเท่าที่ดูจาก html ที่สร้างใน hangman ของ seam ก็เหมือนว่าจะมี id ติดมาเป็น hidden input ด้วย)

ผมทดลองสร้าง Calculator นะครับ อันนี้โมเดลครับ

class Calculator
  attr_accessor :value

  def initialize
    self.value = 0
  end

  def add(x)
    self.value += x
  end

  def sub(x)
    self.value -= x
  end
end

มาดูตอนเอาไปใช้งานก่อนนะครับ ที่ controller ก็ใช้เหมือน ๆ กับ session ครับ

class CalculatorController &lt; ApplicationController
  def index
    reset_context
    @calculator = Calculator.new
    context[:calculator] = @calculator
  end

  def add
    @calculator = context[:calculator]
    @calculator.add(params[:val].to_i)
    render :action => "index"
  end

  def sub
    @calculator = context[:calculator]
    @calculator.sub(params[:val].to_i)
    render :action => "index"
  end

end

แต่ที่แย่หน่อยคือใน view ต้องแปะให้มันซ่อน context_id ติดไปด้วยครับ โดยสั่ง hidden_context_id

<html>
  <body>
    <h1><%= @calculator.value %></h1>

    <% form_tag :action => 'add' do %>
      <%= hidden_context_id %>
      <%= text_field_tag :val %>
      <%= submit_tag &quot;add&quot; %>
    <% end %>

    <% form_tag :action => 'sub' do %>
      <%= hidden_context_id %>
      <%= text_field_tag :val %>
      <%= submit_tag &quot;subtract&quot; %>
    <% end %>
  </body>
</html>

ทีนี้มาดูข้างในกันดีกว่าครับ

ผมปรับแต่งคลาส ApplicationController ซึ่งเป็นคลาสแม่ของทุก ๆ controller ใน app ของ Rails

แนวคิดหลัก ๆ ก็ไม่มีอะไรมาก แค่เพิ่มตัวแปร :context เข้าไปใน session แล้วก็เก็บ context หลาย ๆ อันใน session นั้นโดยใช้ key เป็น context_id

จากนั้นก่อนที่ controller จะโยนงานไปให้แต่ละ action ก็สั่งให้แกะ context ออกมาก่อน

class ApplicationController < ActionController::Base
  helper :all # include all helpers, all the time
  # ... some other stuff ....

  before_filter :get_context_from_request  # สั่งให้แกะ context ก่อน

  attr_reader :context_id    # นิยาม attribute context_id
  attr_reader :context       # นิยาม attribute context

  protected

  def get_context_from_request
    if params[:context_id]
      get_context(params[:context_id])
    else
      @context = nil
    end
  end
  
  def reset_context
    if session[:context]
      @context_id = session[:context].keys.max + 1
    else
      @context_id = 0
      session[:context] = {}
    end
    @context = {}
    session[:context][@context_id] = @context
  end

  def get_context(id)
    id = id.to_i if id.class == String
    @context_id = id
    @context = session[:context][id]
  end

end

แล้วเพิ่มเมท็อด hidden_context_id ใน application_helper.rb ด้วย เพื่อที่จะได้เรียกได้ใน view ครับ

module ApplicationHelper
  def hidden_context_id
    hidden_field_tag :context_id, controller.context_id
  end
end

ตัว conversation context ของ seam น่าจะทำอะไรได้อีกเยอะครับ แต่ถ้าต้องการแค่ให้หลาย ๆ tab ไม่ขึ้นต่อกัน ทำประมาณนี้ก็น่าจะพอได้นะครับ

เพิ่มเติม ในวิธีที่ทำ context โดยเก็บ “ของ” ลงใน session แบบนี้ “ของ” นั้นจะถูก serialized เพื่อให้เก็บลงฐานข้อมูลได้ พอถูกเรียกออกมาค่อยเอาไปประกอบใหม่อีกที นั่นคือของจะเหมือนอยู่ตลอด แต่จริง ๆ แล้ว เกิด-ตาย เกิด-ตาย หลายที ซึ่งถ้า model เราใหญ่ ตรงนี้น่าจะเป็นปัญหามากได้ครับ ผมไม่ทราบว่าใน seam ของจะถูกแปลงไปแปลงมาแบบนี้ด้วยหรือเปล่า หรือว่าจะคงสภาพค้างอยู่ตลอดเลย (ใครรู้ช่วยบอกหน่อยครับ)

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

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

ต่อจากคราวที่แล้วนะครับ

ถ้าจำได้เราได้สร้างความสัมพันธ์แบบ one-to-many ระหว่างวัตถุในโมเดล Writer กับวัตถุในโมเดล Book เวลาจะอ้างถึงรายการของ Book ที่ Writer เป็นเจ้าของ เราก็อ้างผ่านเมท็อด (ที่ทำตัวเหมือน Collection) books ถ้าอ้างจาก Book ก็เรียก author เอา (เดิมทีจะเป็น writer แต่เราไปเปลี่ยนชื่อตอนหลัง)

สาเหตุที่เราอ้างแบบดังกล่าวได้ ก็เพราะว่าเราไปเพิ่ม foreign key writer_id ในตาราง books เพื่อให้วัตถุในโมเดล Book นั้น “belongs_to” วัตถุในโมเดล Writer

ทีนี้เวลาในตารางสองตารางในฐานข้อมูลมีความสัมพันธ์กันขึ้นมา สิ่งหนึ่งที่เราควรต้องใส่ใจเป็นพิเศษก็คือ integrity (ภาษาไทยเรียกว่า “ความบูรณภาพ”) โดยเฉพาะ referential integrity นั่นก็คือแถวที่มีความสัมพันธ์กันต้องเชื่อมโยงกันอย่างถูกต้อง

โดยมากการเชื่อมโยงเวลาเราเขียนใน Active Record ส่วนใหญ่จะไม่ค่อยมีปัญหา เพราะเมื่อเราสั่งคำสั่งพวก

writer.books << newbook
book.author = thiswriter

ก็จะมีการกำหนดค่าใน foreign key ให้อย่างถูกต้องอยู่แล้ว

อย่างไรก็ตาม ปัญหามักเกิดขึ้นเมื่อเราเริ่มลบข้อมูล เช่น เราลบ Writer หมายเลข 3,4,5,6 ไป แต่ไม่ได้ปรับข้อมูลของวัตถุในโมเดล Book นั่นคือยังมีบางแถวในตาราง books ที่ชี้มาที่ Writer หมายเลขเหล่านี้อยู่

ถ้าเราไม่ได้อ้างข้อมูลจากโมเดลดังกล่าว ปัญหาก็ยังอาจจะยังไม่ปรากฏ แต่ถ้าเราเพิ่มวัตถุใหม่เข้าไปในโมเดล Writer ถ้าบังเอิญ (ซึ่งเป็นไปได้) ที่วัตถุเหล่านั้นมีหมายเลขเป็น 5 (ซึ่งโดนลบไปแล้ว) นักเขียนคนนั้นก็คงจะได้หนังสือที่ไม่ได้เขียนมาครอบครอง

วิธีการจัดการกับปัญหาดังกล่าว หรือเรียกว่าการบังคับให้เกิด referential integrity สามารถทำได้ในระดับของ database เช่นการใช้ foreign key constraint ใน MySQL เป็นต้น ถ้าเราไม่ต้องการกำหนดเงื่อนไขดังกล่าวในระดับ database เพราะว่าอาจมีปัญหาความไม่เข้ากันของ database หลาย ๆ ตัว (มีหรือเปล่า?) ใน Rails เราก็พอที่ทำการจัดการดังกล่าวได้ (บางส่วน) โดยเฉพาะเมื่อมีการลบ

ที่กล่าวมาตั้งนานเพื่อจะบอกว่า เวลาเราระบุ has_many หรือ has_one เราสามารถใส่ option :dependent เพิ่มเข้าไปได้เช่น

class Writer < ActiveRecord::Base
  has_many :books, :dependent => :destroy
  # ...
end

เมื่อเราลบคนเขียน john ในโมเดล Writer หนังสือที่ john เป็นเจ้าของก็จะถูกเรียก destroy ไปด้วย

ทดลองดูนะครับ

>> mary = Writer.find_by_first_name("Mary")
=> #<Writer id: 3,.., first_name: "Mary", last_name: "Happy">

>> mary.books.length    #=> 2   (mary เขียน 2)
>> Book.count           #=> 4   (มีหนังสือทั้งหมด 4 เล่ม)
>> mary.destroy         # ทำลาย mary
>> Book.count           #=> 2   (หนังสือหายไปสองเล่มด้วย)

ทีนี้ นอกจากจะกำหนดให้ :dependent เป็น :destroy แล้ว ยังเลือกได้หลายแบบ เช่น ถ้าให้เป็น :nullify ค่าใน foreign key จะถูกกำหนดให้เป็น null หรือถ้ากำหนดให้เป็น :delete หรือ :delete_all (ในกรณี has_many) วัตถุดังกล่าวจะถูกลบออกจากตารางโดยไม่ผ่านการเรียกเมท็อด destroy

แล้วทำไมต้องมีทางเลือกหลายแบบเหลือเกิน? อันนี้จะเป็นการแลกกันระหว่างประสิทธิภาพและความสามารถในการจัดการ ใน Rails เราสามารถกำหนดให้มีการเรียกเมท็อดบางเมท็อดก่อนหรือหลังเหตุการณ์ที่เกิดขึ้นกับวัตถุในโมเดล เช่นก่อนการลบ หรือหลังการลบ เมท็อดเหล่านั้นเรียกว่า callback ซึ่งในการทำงานกับ Active Record เราสามารถเพิ่มเข้าไปได้ อย่างไรก็ตาม ถ้าเราสั่งเช่น :dependent => :delete_all การเรียก callback เหล่านี้จะไม่เกิดขึ้น เพราะว่า Rails จะเข้าไปลบข้อมูลจากฐานข้อมูลโดยตรงเลย ซึ่งจะรวดเร็วกว่าการโหลดข้อมูลมาทั้งหมดแล้วไล่สั่ง destroy ทีละตัว

อ่านเพิ่มเติมได้จากหน้า api ของ Active Record ในส่วน Association หรือจะอ่านจาก guide ก็ได้ครับ

ตอนนี้ค่อนข้างจะเป็นบรรยายเสียเยอะ แต่เรื่องพวกนี้ถ้าจะข้ามไปกลัวว่าเดี๋ยวเขียนไปแล้วจะมีปัญหา (แบบที่ผมเจอเอง อิอิ)

ไว้คราวหน้ากลับมาต่อกับการทำ many-to-many ครับ

หัดเล่น method_missing

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

อย่างที่คุณ SweetCorn ได้เล่าไว้ในคอมเมนต์ของ entry ก่อนเกี่ยวกับการค้นหาด้วย Active Record นะครับ Active Record จัดการกับการเรียกเมท็อดพวก find_by_first_name โดยใช้เมท็อด method_missing สร้างเมท็อดนั้นเพิ่มในขณะทำงาน

ใช้แล้วครับ… เมท็อด method_missing เขียนเมท็อดใหม่เพิ่มลงไปในคลาส ตามแต่เราเรียกใช้

การเขียนโปรแกรมให้เขียนโปรแกรมเพิ่มได้เป็นความสามารถที่พบในภาษาที่เป็น dynamic (ไม่รู้พวกภาษา static จะทำได้หรือเปล่า หรือว่าทำได้แค่ไหน) ซึ่งเรียกรวม ๆ ว่า metaprogramming

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

ปกติภาษาที่เป็น static จะมีการตรวจสอบ type ของวัตถุในขณะคอมไพล์ และจะรับประกันว่าจะไม่มีปัญหา type error ขณะโปรแกรมทำงาน โดยมากแล้วการรับประกัน type นี่ก็จะรวมไปถึงว่าจะรับประกันด้วยว่าเวลาเราเรียกเมท็อด M ที่ตัวแปร O ตัวนี้ คลาสของตัวแปร O จะต้องรองรับเมท็อด M ดังกล่าว ดังนั้นถ้ามีการเรียกเมท็อดที่วัตถุ ถ้าวัตถุนั้นไม่ได้มีการรองรับเมท็อดนั้นไว้ สิ่งที่ได้คือ compiler message

ในขณะที่ภาษา dynamic ไม่ได้มีการตรวจสอบ type ขณะคอมไพล์ (ถ้าจะมีการคอมไพล์) แต่จะตรวจสอบขณะที่โปรแกรมทำงาน ดังนั้นถ้ามีการเรียกเมท็อดที่วัตถุ ถ้าวัตถุนั้นไม่ได้มีการรองรับเมท็อดนั้นไว้ สิ่งที่ได้คือ run-time error ซึ่งจะเกิดขึ้นตอนโปรแกรมทำงาน

อย่างไรก็ตาม ในภาษาเช่น Ruby วัตถุสามารถ “รู้ตัว” ได้ว่ามีการเรียกเมท็อดที่ไม่ได้นิยามเอาไว้ได้ (เท่าที่เข้าใจ ความสามารถนี้มีมาตั้งแต่ Smalltalk แล้ว)

เป้าหมายของการที่ทำให้วัตถุรู้ตัวดังกล่าวได้นั้น จากการที่ผมเกริ่นมาข้างต้น หลายคนอาจคิดว่ามีเอาไว้เพื่อจัดการกับปัญหา run-time error แต่จริง ๆ การเรียกเมท็อดที่ไม่ได้นิยามเอาไว้ ในภาษาเช่น Ruby เป็นเรื่องที่ทำกันเป็นปกติ ไม่ใช่เพราะว่ามีปัญหาว่าเราเขียนโปรแกรมผิดเพียงอย่างเดียว

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

เรามาทดลองอะไรง่าย ๆ กันก่อน ด้านล่างเป็นคลาสที่ไม่ได้ประกาศอะไรไว้เลย ยกเว้นเมท็อด method_missing

class AllMissing
  def method_missing(name, *args)
    puts "method: #{name.to_s}"
    puts "arguments are: "
    args.each { |arg| puts "#{arg}" }
  end
end

เมท็อดข้างต้นรับ name ซึ่งชื่อเมท็อดที่ถูกเรียกจะถูกส่งมาเป็น symbol ครับ ดังนั้นก่อนจะเอาไปใช้ ถ้าจะใช้เป็น string จะต้องสั่ง to_s (ในกรณีนี้เราเอาไปพิมพ์ จริง ๆ ไม่ต้องแปลงก็ได้) ส่วนอีกอาร์กิวเมนท์หนึ่งสังเกตว่าขึ้นต้นด้วย * อันนี้เป็นการระบุในภาษา Ruby ว่าให้รับอาร์กิวเมนท์หลายตัวได้ แล้วให้ส่งมาเป็นลิสต์ครับ ซึ่งเราเอามาสั่งพิมพ์ (บรรทัด args.each ...)

มาทดลองกันนะครับ ลองเรียก irb แล้วเอาโค้ดข้างบนไปแปะเพื่อประกาศคลาส AllMissing ในนั้นก่อนนะครับ

irb(main):008:0> a = AllMissing.new
=> #<AllMissing:0xb7bf2338>

irb(main):009:0> a.hello
method: hello
arguments are:
=> []

irb(main):010:0> a.hello "this is", "my world"
method: hello
arguments are:
this is
my world
=> ["this is", "my world"]  # เมท็อดคืนค่าออกมาด้วย

irb(main):011:0> a.goodbye 1,2,3,4
method: goodbye
arguments are:
1
2
3
4
=> [1, 2, 3, 4]  # เมท็อดคืนค่าออกมาด้วย

สังเกตว่ามีค่าที่คืนมาจากเมท็อดด้วย เพราะว่า Ruby ถ้าไม่ระบุคำสั่ง return จะคืนค่าสุดท้ายของการทำงานในเมท็อดออกมา

มาเขียนอะไรเล่นสนุก ๆ ดีกว่าครับ

class Dog
  def jump
    puts "Jump"
  end

  def bark
    puts "Box Box"
  end

  def method_missing(name, *args)
    actions = name.to_s.split("_and_")
    actions.each do |action|
      self.send(action.to_sym)
    end
  end
end

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

ไปทดลองใน irb เช่นเคย

irb(main):077:0> d = Dog.new
=> #<Dog:0xb7aba5ec>
irb(main):078:0> d.jump
Jump

irb(main):079:0> d.jump_and_bark
Jump
Box Box

irb(main):080:0> d.jump_and_bark_and_jump_and_jump
Jump
Box Box
Jump
Jump

แต่ต้องระวังนะครับ ถ้าเราสั่ง d.jump_and_run ผลที่ได้คือ:

SystemStackError: stack level too deep
	from (irb):28:in `send'
	from (irb):28:in `method_missing'
	from (irb):27:in `each'
	from (irb):27:in `method_missing'
	from (irb):28:in `send'
	from (irb):28:in `method_missing'
	... อีกยาวเหยียด

เพราะว่าเราไม่ได้นิยามเมท็อด run เอาไว้ เมท็อด method_missing กับ send จึงเรียกกันไปกันมาสลับกันไม่รู้จบ ดังนั้นก่อนจะเอาไปเล่นจริง ๆ เราควรตรวจสอบสักนิด อย่างเช่นด้านล่าง

  def method_missing(name, *args)
    actions = name.to_s.split("_and_")
    if actions.length==1
      raise NoMethodError, "#{actions[0]}"
    end
    actions.each do |action|
      self.send(action.to_sym)
    end
  end

irb(main):103:0> d.jump_and_jump_and_run
Jump
Jump
NoMethodError: run
	from (irb):95:in `method_missing' ...

ดูตัวอย่างอีกอันที่ผมขโมยมาจากบล็อก Ola Bini ซึ่งหยิบมาจาก Camping อีกที

class Hash
  def method_missing(m,*a)
    if m.to_s =~ /=$/
      self[$`] = a[0]
    elsif a.empty?
      self[m]
    else
      raise NoMethodError, "#{m}"
    end
  end
end

อันนี้เป็นการเขียนเพิ่มเข้าไปในคลาส Hash มาตรฐานของ Ruby ครับ ทำให้สามารถเรียกของใน Hash ได้เหมือนฟังก์ชัน เช่นแทนที่จะเรียก a[:hello] ก็เรียกได้เป็น a.hello หรือแทนที่จะสั่ง a[:hello] = 10 ก็สั่ง a.hello=10 ได้เลย

ผลเวลาทำงานครับ

x = {'abc' => 123}
x.abc # => 123
x.foo = :baz
x # => {'abc' => 123, 'foo' => :baz}

ในเมท็อดข้างต้นถ้าจะดูยากหน่อยก็ตรงส่วนการใช้ regular expression (ตรงแถว ๆ /=$/) นะครับ แต่น่าจะพอเดา ๆ ได้

วันนี้เล่นกันแค่นี้ก่อน ขอจบด้วยลิงก์ครับ

ด้านล่างผมฝากลิงก์บทความที่เกี่ยวข้องกับ metaprogramming เอาไว้ เผื่อใครอยากจะไปลองเล่นก่อน (แล้วเอามาเขียนด้วยก็ได้นะครับ)

เพิ่มเติม:
คุณ DominixZ ช่วยอธิบายเพิ่มเติมว่าทำไมเมท็อดด้านบน (ส่วน Dog) สามารถตรวจสอบการเรียกซ้ำไว้ได้ที่ comment นะครับ ผมขอคัดลอกมาที่นี่เลยนะครับ: “ที่ method นี้ตรวจสอบได้ เพราะเวลาเรา send(:symbol) ไปแล้วมันไม่เจอมันจะกลับมาทำงานที่ method_missing อีกแล้วเงือนไขที่เรา check length ถ้ามันมีตัวเดียวหมายความว่า method นั้นไม่มี เพราะมันเข้ามาใน misssing_method นั้นเองครับ”

Follow

Get every new post delivered to your Inbox.