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

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

Advertisements

สนุกกับ 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 แทน

ทำปลอม 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&#91;:context_id&#93;
      get_context(params&#91;:context_id&#93;)
    else
      @context = nil
    end
  end
  
  def reset_context
    if session&#91;:context&#93;
      @context_id = session&#91;:context&#93;.keys.max + 1
    else
      @context_id = 0
      session&#91;:context&#93; = {}
    end
    @context = {}
    session&#91;:context&#93;&#91;@context_id&#93; = @context
  end

  def get_context(id)
    id = id.to_i if id.class == String
    @context_id = id
    @context = session&#91;:context&#93;&#91;id&#93;
  end

end
&#91;/sourcecode&#93;

แล้วเพิ่มเมท็อด <tt>hidden_context_id</tt> ใน <tt>application_helper.rb</tt> ด้วย เพื่อที่จะได้เรียกได้ใน 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 นั้นเองครับ”

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