Friday, August 26, 2005

ทดลอง acts_as_tree feature ของ ActiveRecord (Ruby on Rails)

สรุปให้ฟังสำหรับคนที่ไม่อยากลงรายละเอียด
ใน post นี้ ประกอบด้วย 3 ประเด็นก็คือ
  • Feature ของ ActiveRecord ที่ใช้ในการกำหนดความสัมพันธ์แบบ self relate
    (link เข้าหาตัวเอง, tree structure)
    acts_as_tree
  • Rails Migration Tool
    เป็น framework เล็กๆที่ช่วยในการ maintain database structure
    ช่วยให้เราสามารถ keep สถานะ table structure เราในลักษณะ version ได้
    (ทำให้เราถอยหลังหรือเดินหน้าไปที่ version ที่ต้องการได้)
    Active Record Migration
  • Model Unit TestCase
    การเขียน TestCase สำหรับทดสอบการทำงานของ Model
    Testing Your Models
    Fixtures


Details
วันนี้จะทดลองเล่น rails โดยทดลอง model Object ที่ self relate เข้าหาตัวเอง
(มีลักษณะเป็น Tree Structure)
โดยจะใช้ตัวอย่าง business object ที่ชื่อ Category

เริ่มด้วยการสร้าง Model file ที่ชื่อ $PROJECT/app/models/category.rb
ซึ่งทำได้โดยสั่งคำสั่ง script/generate model category
ให้เพิ่มเนื้อหาของ file เข้าไปดังนี้
class Category < ActiveRecord::Base
acts_as_tree :order => "name"
end

parameter order เป็นการกำหนดการเรียงลำดับของ children node
(ในตอน qurey)

เมื่อมี model แล้ว ก็ต้องทำการสร้าง Table ใน Database ด้วย
สำหรับการสร้าง table นั้น rails มี feature หนึ่งที่ช่วยให้เรา
maintain structure ของ table ในลักษณะ keep version ได้
feature นั้นก็คือ Migration

เริ่มด้วยการใช้ command script/generate migration table
เพื่อทำการ generate file ที่ชื่อ $VERSION_table.rb ให้เรา
โดย file จะอยู่ใต้ directory $PROJECT/db/migrate
($VERSION จะใช้แสดง version number ที่จะ automatic เรียงลำดับขึ้นไปเรื่อยๆ)

กรณีนี้เราทำเป็นครั้งแรก ดังนั้นจะได้ file 1_table.rb
ให้เราทำการ edit file ให้มีเนื้อหาดังนี้
class Table < ActiveRecord::Migration
def self.up
create_table :categories do |t|
t.column :name, :string
t.column :parent_id, :integer
end
end

def self.down
drop_table :categories
end
end

ความหมายก็คือ สร้าง table ที่ชื่อ categories โดยมี
column name, parent_id (foreign key ที่ชี้เข้าหาตัวเอง)
และมี primary key บน column id (migrate default สร้างให้เอง)

Note: ความหมายของ down method ก็คือ
กรณีที่มีการย้อน version จะต้องใส่ script
ที่ช่วยในการ reverse สิ่งที่เราสั่งทำงานไปใน up method


จากนั้นเมื่อต้องการสั่งให้ migration script ทำงาน
ก็ให้ใช้คำสั่ง rake migrate

Step ถัดไปก็คือ การทดสอบว่า model ของเราทำงานได้ถูกต้องไหม
โดยการเขียน Test Unit ที่ชื่อ categories_test.rb ไว้ใต้ directory
$PROJECT/test/unit
require File.dirname(__FILE__) + '/../test_helper'

class CategoryTest < Test::Unit::TestCase
fixtures :categories

def test_select
root = Category.find_first "name = 'root'"
s1 = root.children[0]
assert_equal @categories["sub1"]["name"], s1.name
end

def test_root
root = Category.new();
root.name = "root"

assert root.save

c1 = root.children.create("name" => "pok")
c2 = root.children.create("name" => "bunn")

assert_equal c1.parent, root
assert_equal c2.parent, root
end
end

Note: รูปแบบการเขียน testcase จะเหมือนกับ JUnit

ใน TestCase ที่เราเขียน จะเห็นว่ามีการกำหนด Fixtures ไว้ด้วย
(fixture ก็คือ set ของ Data ที่เราจะ populate ลง table ไว้ก่อนที่
จะเริ่มทำการทดสอบ)
โดยตำแหน่งของ fixture จะสร้างไว้ใต้ $PROJECT/test/fixtures
โดย Rails เปิดให้เราเลือกใช้ fixtures file ได้ 2 แบบก็คือ

  • yaml
  • csv (comma seperated)

กรณีของเราเลือกใช้ yaml เนื่องจากกรณีทีใช้ csv จะเกิดปัญหา
กับ ค่า null ใน column ที่เป็น integer

ตัวอย่าง fixtures ที่ใช้
root:
id: 1
name: root
sub1:
id: 2
name: sub1
parent_id: 1
sub2:
id: 3
name: sub2
parent_id: 1


ที่นี้ลองมาดูว่า สมมติว่าเราเพิ่ม feature การ maintain children count
ใน table categories ของเรา แล้วจะใช้ migration เข้ามาช่วย
update table structure ของเราได้อย่างไรบ้าง

ขั้นแรก ก็คือการปรับ model เรา โดยการเพิ่ม option เข้าไปใน acts_as_tree ดังนี้
class Category < ActiveRecord::Base
acts_as_tree :order => "name", :counter_cache => "true"
end

option counter_cache จะใช้ column categories_count ในการเก็บจำนวน
ของ children node ที่เป็นสมาชิกของ node นั้นๆ

จากนั้น ก็ generate migration script ขึ้นมา
โดยการสั่ง script/generate migration add
ผลลัพท์ที่ได้ก็คือ file $PROJECT/db/migrate/2_add.rb

ให้เราทำการ edit file เพิ่มเนื้อหาเข้าไปดังนี้
class Add < ActiveRecord::Migration
def self.up
# on mysql
# add_column :categories, :categories_count, :integer, default => 0

# on postgres
# postgres has no alter table add column with default value
# so we must do it manually.
add_column :categories, :categories_count, :integer
execute "alter table categories alter categories_count set default 0"
end

def self.down
remove_column :categories, :categories_count
end
end


Note: มี bug บน Rails ที่เกิดจาก Postgres Database ไม่ได้ implements
sql alter table with default value ทำให้เราต้อง manual set ค่า default
ตามหลังเข้าไปอีกที


จากนั้นก็สั่ง rake migrate เพื่อทำการ update table struture
ให้มี version ล่าสุดตาม script ใน migrate directory

Note
ตัว Migration เท่าที่ลองเล่นดู จะเกิดปัญหาขึ้นประปราย ซึ่งเท่าที่เจอก็เช่น
พอเราสั่ง migrate เดินหน้าหรือถอยหลัง กับ script ที่มี syntax ไม่ถูกต้อง
จะเกิดปัญหา inconsistent ขึ้นใน meta data ที่ใช้เก็บข้อมูล current version
ทำให้เราต้องใช้ manual sql เข้าไปจัดการกับ meta data โดยตรง

Related link from Roti

No comments: