การใช้งาน 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==""
      flash[:notice] = "Error bad title"
      render :action => 'new'
      return
    end
    if @book.pages==""
      flash[:notice] = "Error bad page number"
      render :action => 'new'
      return
    end
    if @book.title[0]  ?Z
      flash[:notice] = "Error title should begin with cap"
      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 แทน

สนุกกับ 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 ครับ

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

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

ก่อนจะเริ่ม เรามาอธิบายของเก่าเพิ่มเติมสักนิด (อ้างมาจากคอมเมนท์ใน entry ใน grails66 ครับ)

เวลาเราสั่ง find(:first) ในกรณีที่มีคำตอบหลายอัน หลายคนอาจสงสัยว่า Active Record จะคืนตัวไหนมาให้ อย่างไรก็ตามในเอกสารไม่มีการระบุว่าจะตอบอันไหนก่อนหลัง แต่ระบุว่าจะเรียงมาตามที่ระบบฐานข้อมูลคืนมาให้ครับ

แต่ไม่ค่อยเป็นปัญหาเท่าใด เพราะส่วนมากเรานิยมใช้ :first ประกอบกับ option อื่น ๆ เช่น

   ## หาหนังสือที่พิมพ์มาเก่าสุด
>> Book.find(:first, :order => "published_at")
=> #<Book id: 4, title: "Algorithms", ..,published_at: "2006-05-01",..>

   ## หาหนังสือที่บางที่สุด
>> Book.find(:first, :order => "pages")
=> #<Book id: 3, title: "AJAX Tricks", pages: 300, ..>

มาเข้าเรื่องกันดีกว่าครับ

หลังจากที่เรามีโมเดล Writer กับ Book มาแล้ว (ดูตอนก่อน ๆ: 1, 2, 3) เดี๋ยวเราจะโยงให้มันสัมพันธ์กัน

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

อาจจะฟังแล้วงง ลองดูตัวอย่างดีกว่าครับ ในกรณีของโมเดล Writer กับ Book สังเกตว่า Writer หนึ่งคน สามารถเขียนหนังสือได้หลายเล่ม (ขอเว้นกรณีที่หนังสือหลายเล่มเขียนโดยคนเขียนหลายคนเอาไว้คราวหน้า)

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

แล้วเราจะใส่อะไรที่ทำให้ “อ้างอิง” ไปยังคนเขียนได้? ถ้าในชีวิตจริงเราก็จะใส่ชื่อ-นามสกุล แต่ในกรณีของเรา แต่ละวัตถุในโมเดลมีเลข id เอาไว้อ้างอิงโดยไม่ซ้ำกันอยู่แล้ว ดังนั้นหน้าตาของสองโมเดลที่เชื่อมโยงกันจะเป็นดังด้านล่างครับ

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

ข้อตกลงใน Rails ก็คือ ถ้าคอลัมน์นี้อ้างอิงไปยังคลาส Writer คอลัมน์จะต้องชื่อว่า writer_id (เช่นเคย ถ้าไม่อยากทำตามข้อตกลงก็ปรับแต่งได้)

เราจะไปเพิ่มคอลัมน์ดังกล่าว การปรับตารางก็ทำเหมือนเช่นเคย คือสร้าง migration ด้วยคำสั่ง ./script/generate migration add_writer_id_to_books แล้วแก้โปรแกรมใน migration ดังด้านล่างครับ

class AddWriterIdToBooks < ActiveRecord::Migration
  def self.up
    add_column "books", "writer_id", :integer
  end

  def self.down
    remove_column "books", "writer_id"
  end
end
&#91;/sourcecode&#93;

เสร็จแล้วก็สั่ง <tt>rake db:migrate</tt> เพื่อปรับฐานข้อมูล

จากนี้เราจะทดลองกันไปก่อน แล้วค่อยสรุปว่าหลักการทั่วไปเป็นอย่างไรนะครับ

เราจะมาทำตามเป้าหมายกันครับ คือให้ <tt>Writer</tt> หนึ่งคน มีหนังสือได้หลายเล่ม  ดังนั้นในโมเดล <tt>Writer</tt> เราจะเพิ่มบรรทัดด้านล่างลงไป


class Writer < ActiveRecord::Base
  has_many :books

  # ... ละส่วนอื่น ๆ ไว้ครับ
end
&#91;/sourcecode&#93;

การประกาศดังกล่าวจะสร้าง attribute ที่ชื่อว่า <tt>books</tt> ในโมเดล <tt>Writer</tt> โดย attribute นี้จะใช้งานได้เหมือน collection ของวัตถุจากโมเดล <tt>Book</tt>

ไปทดลองกันที่ console เลยครับ
<pre>&gt;&gt; <strong>mary = Writer.find_by_first_name("Mary")</strong>   #=&gt; #&lt;Writer <strong>id: 3</strong>, .., "Mary",..&gt;

&gt;&gt; <strong>mary.books</strong>   #=&gt; [] ตอนแรกยังไม่มีหนังสือเลย

&gt;&gt; <strong>Book.find(:all)</strong>   # เอารายการหนังสือมาดูก่อน
=&gt; [#&lt;Book id: 1, title: "Ruby on Rails", .., writer_id: nil&gt;,
#&lt;Book id: 2, title: "Ruby Programming", .., writer_id: nil&gt;,
#&lt;Book id: 3, title: "AJAX Tricks", .., writer_id: nil&gt;,
#&lt;Book id: 4, title: "Algorithms", .., writer_id: nil&gt;]

   ## เลือกเล่มนี้ละกัน
&gt;&gt; <strong>ror = Book.find_by_title "Ruby on Rails"</strong>
=&gt; #&lt;Book id: 1, title: "Ruby on Rails", .., writer_id: nil&gt;

   ## ใส่ ror ลงใน collection books
&gt;&gt; <strong>mary.books &lt;&lt; ror</strong>
=&gt; [#&lt;Book id: 1, title: "Ruby on Rails", .., <strong>writer_id: 3</strong>&gt;]

   ## เพิ่ม "Ruby Programming" ใน collection books อีกเล่ม
&gt;&gt; <strong>mary.books &lt;&lt; Book.find_by_title("Ruby Programming")</strong>
=&gt; [#&amp;Book id: 1, title: "Ruby on Rails", .., writer_id: 3&gt;,
#&lt;Book id: 2, title: "Ruby Programming", .., writer_id: 3&gt;]

   ## ไล่หนังสือของ mary มาพิมพ์ดู
&gt;&gt; <strong>mary.books.each do |book|</strong>
?&gt;   <strong>puts "#{book.title}, #{book.pages} pages"</strong>
&gt;&gt; <strong>end</strong>
Ruby on Rails, 450 pages
Ruby Programming, 320 pages
=&gt; [#&lt;Book id: 1, ...]   # อันนี้เป็นค่าที่ return จากคำสั่งด้านบน ไม่ใช่ผลลัพธ์ที่พิมพ์ออกมา</pre>
สังเกตว่า เมื่อเราเพิ่ม <tt>Book</tt> เข้าไปในรายการของ <tt>mary</tt> เราจะพบว่า foreign key ของหนังสือแต่ละเล่มจะถูกเปลี่ยนไปให้กลายเป็นหมายเลขประจำตัวของ <tt>mary</tt> นอกจากนี้ถ้าเราเพิ่มการอ้างอิงโดยการจับใส่ใน collection <tt>books</tt> บรรดาวัตถุที่ถูกเพิ่มเข้าไปเหล่านั้นจะถูก save โดยอัตโนมัติ

ทีนี้ในทางกลับกัน เราอาจอยากรู้บ้างว่าหนังสือเล่มนี้ใครเขียน?
<pre>&gt;&gt; <strong>ror.writer</strong>
NoMethodError: undefined method `writer' for #&lt;Book:0xb7016ed8&gt;
	from /../activerecord-2.1.0/lib/active_record/
	     attribute_methods.rb:256:in `method_missing'
	from (irb):2</pre>
ปรากฏว่าไม่สามารถสั่งได้ เนื่องจากเรายังไม่ได้ปรับโปรแกรมของโมเดล

อย่างไรก็ตาม เนื่องจากใน Ruby เราสามารถเพิ่ม/แก้ไขคลาส ได้ขณะที่เรากำลังทำงาน ดังนั้นเราจะเพิ่มการประกาศความเชื่อมโยงกับโมเดล <tt>Writer</tt> ในคลาส <tt>Book</tt> ภายใน console เลย ดังนี้ครับ
<pre>&gt;&gt; <strong>class Book</strong>
&gt;&gt;   <strong>belongs_to :writer</strong>
&gt;&gt; <strong>end</strong></pre>
จากนั้นทดลองดูนะครับ
<pre>&gt;&gt; <strong>ror.writer</strong>
=&gt; #&lt;Writer id: 3, .., first_name: "Mary", last_name: "Happy"&gt;</pre>
จาก <tt>Book</tt> อ้างถึง <tt>Writer</tt> ได้แล้ว

สังเกตว่าแม้เราจะเพิ่งแก้ไขคลาสไป และไม่ได้สร้างวัตถุอันใหม่มาใส่ให้กับตัว <tt>ror</tt> วัตถุของคลาส <tt>Book</tt> ก็เหมือนจะมีความสามารถใหม่ที่เราเพิ่มเข้าไปทันที  อันนี้เป็นความสามารถ (หรือบางคนบอกว่าเป็นปัญหา) ของภาษา Ruby  ที่เรียกว่า open classes  นั่นคือสำหรับคลาสใด ๆ คลาสนั้นสามารถ "เปิด" ออกมาเพื่อเพิ่มหรือแก้ไข "ของ" ในคลาสได้ตลอดเวลา

เรามาทดลองอะไรกันอีกสักนิดครับ
<pre>   ## หาหนังสืออีกสักเล่ม
&gt;&gt; <strong>algo = Book.find_by_title("Algorithms")</strong>
=&gt; #&lt;Book id: 4, title: "Algorithms", .., writer_id: nil&gt;

   ## หานักเขียนมาสักคน
&gt;&gt; <strong>john_bestman = Writer.find_by_last_name("Bestman")</strong>
=&gt; #&lt;Writer id: 2, .., first_name: "John", last_name: "Bestman"&gt;

   ## เชื่อมโยง
&gt;&gt; <strong>algo.writer = john_bestman</strong>
=&gt; #&lt;Writer id: 2, .., first_name: "John", last_name: "Bestman"&gt;

   ## ทดสอบอีกข้าง?   อ้าวไม่มา..
&gt;&gt; <strong>john_bestman.books</strong>
=&gt; []

   ## เพราะว่าไม่ได้ save นี่เอง
&gt;&gt; <strong>algo.save</strong>
=&gt; true

   ## อ้าว ยังไม่มาอีก??
&gt;&gt; <strong>john_bestman.books</strong>
=&gt; []

   ## อ้อ... ต้อง reload ก่อนนี่เอง
&gt;&gt; <strong>john_bestman.books(true)</strong>
=&gt; [#&lt;Book id: 4, title: "Algorithms", .., writer_id: 2&gt;]</pre>
สังเกตว่าเวลาเราใส่การเชื่อมโยงจากอีกข้าง (ในกรณีนี้คือจากทางโมเดล <tt>Book</tt>) ความสัมพันธ์ดังกล่าวจะไม่ถูก save อัตโนมัติ    นอกจากนี้ในตัวอย่างด้านบนยังแสดงการระบุให้เวลาอ้างถึงการเชื่อมโยงต้องมีการ reload เสียก่อน โดยใส่อาร์กิวเมนท์ <tt>true</tt> เข้าไป  (ถ้าดูในการประกาศเมท็อด เราจะพบว่าจะมีอาร์กิวเมนท์ปกติเป็น <tt>false</tt> อยู่)

พอทดลองเสร็จ เราก็ไปแก้โมเดล <tt>Book</tt> ในแฟ้ม <tt>app/models/book.rb</tt> ครับ


class Book < ActiveRecord::Base
  belongs_to :writer
end
&#91;/sourcecode&#93;

แต่จริง ๆ เวลาเรียก "คนเขียน" จากหนังสือ เราไม่ได้เรียกว่า <tt>writer</tt> แต่เราเรียกว่า <tt>author</tt> ในการระบุความสัมพันธ์ <tt>belongs_to</tt> จากข้างต้นสามารถปรับแต่งได้ครับ เราเขียนใหม่เป็น


class Book < ActiveRecord::Base
  belongs_to :author,
             :class_name => "Writer",
             :foreign_key => "writer_id"
end

ทดลองอีกสักหน่อยใน console

>> algo = Book.find_by_title("Algorithms")
=> #<Book id: 4, title: "Algorithms", .., writer_id: 2>
>> algo.author
=> #<Writer id: 2, .., first_name: "John", last_name: "Bestman">

พอดูดีขึ้นแล้วนะครับ

เรามาสรุปแนวคิดกันนิดหน่อยก่อนจะจบตอน

สิ่งที่ชวนให้สับสนมากก็คือ เมื่อใดจะใช้ belongs_to เมื่อใดจะใช้ has_many

Rails จะมองว่า โมเดล A ที่สร้าง foreign key ไปยังโมเดล B นั้น จะมองว่า “เป็นของ” (belongs_to) โมเดล B

ยกตัวอย่างเช่น ถ้าเรามีโมเดล Car กับโมเดล Driver คนขับคนหนึ่งมีรถได้หลายคัน พอเราไปสร้าง foreign key ที่โมเดล Car ให้อ้างมาที่โมเดล Driver แล้ว เราจะมองว่า “รถเป็นของคนขับ”

                             --------------
  -------------                   Car
      Driver                 ==============
  =============              - id
  - id         <------------ - driver_id
  - first_name               - brand
  - last_name                - model
  -------------              - year
                             --------------

เมื่อรถเป็นของคนขับ ดังนั้น คนขับก็จะมีรถ ใน Rails เรามีรถได้ “หลายคัน” (has_many) หรืออาจจะมีได้คันเดียว (has_one)

สังเกตว่า เราใช้ belongs_to ในโมเดลที่เก็บตัวอ้างอิง (foreign key) เอาไว้ ส่วนในอีกโมเดลที่ถูกอ้างถึง เราจะใช้ has_many หรือ has_one

ถ้าเราลองคิดว่า Active Record ทำงานอย่างไรเวลาอ้างถึง อาจจะทำให้เราเห็นภาพมากขึ้นครับ ในกรณีของ belongs_to นั้น Active Record ก็จะดู foreign key จาก field ในวัตถุ แล้วก็ไปเรียกเอาวัตถุที่ถูกอ้างถึงมา (อยู่ที่รถ เปิดสมุดคู่มือรถ แล้วเดินไปเรียกเจ้าของรถมา)

ส่วนในกรณีของ has_many ในโมเดลที่เราเรียกไม่ได้มี foreign key ใด ๆ แต่เราจะไปค้นในโมเดลที่ถูกอ้างถึงนั้น เพื่อหาวัตถุที่มี foreign key ชี้มาที่วัตถุตั้งต้นของเรา (อยู่กับคนขับ ถามว่า มีรถคันใดบ้าง ที่คนขับคนนี้เป็นเจ้าของ)

คราวหน้าเราจะมาทดลอง/ศึกษาเกี่ยวกับการเชื่อมโยงเพิ่มเติม ทั้งในเรื่องการขึ้นต่อกันเวลาเราลบวัตถุในโมเดลหนึ่ง และการเชื่อมโยงแบบ many-to-many ครับ