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

Tuesday, August 23, 2005

Link น่าสนใจ (2005-08-23)


  • Metro Arts and Architecture
    เห็นแล้ว รถไฟฟ้าใต้ดินเราจืดสนิดเลย
    เขาแบ่งกลุ่มดังนี้ (รู้แล้วจะได้ไช้เป็นเกณฑ์ในการมองกลับมาที่บ้านเราบ้าง)
    • พวก Consistently plan, user-friendly, good-looking
    • พวกที่มี สถาปัตยกรรมโดดเด่น
    • พวกที่ตบแต่งภายในโดดเด่น
    • พวกที่มีลักษณะ "Palace for the people" กลุ่มนี้เป็นประเทศพวกอดีตสังคมนิยม
    • พวกที่มี public art

    ที่ผมชอบก็มี Tashkent, stockholm, Los Angeles, Lille, berlin

  • art.com
    ดูเผินๆ ก็เป็น ui ที่เปิดโอกาสให้เราวาดรูปธรรมดา (interface สวยดีเหมือนกัน)
    แล้วก็ให้คน submit รูปที่วาดเข้าไปได้
    แต่ที่น่าสนใจจริงๆก็คือวิธีที่เขา record event ไว้
    ทำให้เวลาดูรูปของคนอื่น(ที่ submit เข้าไปใน gallery)
    เราก็จะเห็น process ที่เขา develop รูปเขาขึ้นมา
    ซึ่งอันนี้น่าสนใจมาก
    (จริงๆแล้วในแง่ software ก็น่าสนใจมากเช่นกัน
    ถ้าต้องเขียนโปรแกรมให้ record ให้ได้แบบเขา
    จะต้องเขียนอย่างไร)
    อย่าลืมเข้าไปดู gallery (replay mode)

  • The Sky in a Room

  • Lilac Chaser
    เปิดเข้าไปแล้วจะเห็น วงสีที่กระพริบเปลี่ยนตำแหน่งอยู่ ให้มองที่จุดศูนย์กลาง
    แล้วก็จะเห็นว่าปรากฎการณ์ “negative retinal afterimage”

Related link from Roti

Monday, August 22, 2005

Message Driver Bean with Spring

ActiveMQ เป็น Opensource JMS provider ตัวหนึ่งทีี่มีคนนิยมใช้
เช่น Geronimo ก็เอาไป integrate เป็น Message Service ของตัวเอง

ใน ActiveMQ มี sub project ตัวหนึ่งที่ชื่อ Jencks
ทำหน้าที่เป็น lightweight JCA container
สามารถ config เข้าไปใน Spring Framework เพื่อที่จะให้บริการ Message Driver Bean ได้
(โดยไม่ต้องมี J2ee Application Server)

Message Bean


ทดลองเขียน Message Bean ง่ายๆ
package mx.test.mq;

import java.util.Enumeration;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

public class EchoBean implements MessageListener {

public void onMessage(Message msg) {
try {
TextMessage txt = (TextMessage) msg;
System.out.println(txt.getText());
} catch (JMSException je) {
je.printStackTrace();
}
}
}

จากนั้นก็ add Container เข้าไปใน Spring
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org
/dtd/spring-beans.dtd">
<beans>
<bean id="jencks" class="org.jencks.JCAContainer">
<property name="bootstrapContext">
<bean class="org.jencks.factory.BootstrapContextFactoryBean">
<property name="threadPoolSize" value="25"/>
</bean>
</property>

<property name="resourceAdapter">
<bean id="activeMQResourceAdapter" class="org.activemq.ra.ActiveMQRe
sourceAdapter">
<property name="serverUrl" value="tcp://localhost:61616"/>
</bean>
</property>
</bean>

<bean id="monitor" factory-method="addConnector" factory-bean="jencks" singl
eton="true">
<property name="activationSpec">
<bean class="org.activemq.ra.ActiveMQActivationSpec">
<property name="destination" value="Monitor.Queue"/>
<property name="destinationType" value="javax.jms.Queue"/>
</bean>
</property>

<property name="ref" value="echoBean"/>
</bean>

<bean id="echoBean" class="mx.test.mq.EchoBean" singleton="true"/>

</beans>

จากนั้นก็ทดลองเขียน Java Class ที่ใช้ในการ start service
package mx.test.mq;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

/**
* @param args
*/
public static void main(String[] args) {
ClassPathXmlApplicationContext appContext =
new ClassPathXmlApplicationContext(
new String[] {"beans.xml"});
}

}


ทดลองเขียน Client เพื่อส่ง Message


เราจะใช้ Spring JmsTemplate เข้ามาช่วย
โดยเขียน Spring Config file ดังนี้
<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org
/dtd/spring-beans.dtd">
<beans>
<bean id="jmsFactory" class="org.activemq.ActiveMQConnectionFactory">
<property name="brokerURL">
<value>tcp://localhost:61616</value>
</property>

</bean>

<!-- Spring JMS Template -->
<bean id="jms" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory">
<ref local="jmsFactory"/>
</property>
<property name="defaultDestinationName" value="Monitor.Queue"/>
</bean>
</beans>

ทดลองส่ง Message ดังนี้
package mx.test.mq;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;

import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;

public class Client {

/**
* @param args
*/
public static void main(String[] args) {
ClassPathXmlApplicationContext appContext = new ClassPathXmlApplicationC
ontext(
new String[] { "client.xml" });
JmsTemplate jms = (JmsTemplate) appContext.getBean("jms");
jms.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("hello queue world");
}
});
}

}


Note: ก่อนจะ run อย่าลืม start ActiveMq ให้เรียบร้อยก่อน

เท่าที่ทดสอบดู ก็ work ok ดี แต่ไม่ค่อยชอบอยู่จุดหนึ่ง ตรง dependency file
ที่ต้องใช้ ลองดูว่าต้องใช้อะไรบ้าง
* connector-api.jar -> JCA api
* geronimo-transaction.jar
* geronimo-connector.jar
* geronimo-kernel.jar
* geronimo-j2ee.jar
* jencks.jar
* activemq-ra.jar
* activemq-core.jar
* geronimo-spec-jms.jar -> JMS api
* geronimo-spec-j2ee-management.jar (javax.management.j2ee)
* concurrent-1.3.4.jar
* commons-logging.jar
กลุ่มล่างๆไม่ติดใจอะไร เพราะเป็น Spec Api ที่ต้อง import อยู่แล้ว
แต่กลุ่มบน จะเห็นว่า dependency กับ Geronimo เยอะมาก

ที่สนใจ JMS ตอนนี้ก็เพราะว่า สามารถเอามาใช้เป็น infrastructure ใน
การ integrate rails, java เข้าหากันได้ (ผ่านทาง TTMP protocol ของ ActiveMQ)
โดยให้ rails เป็นส่วน Web Application และใช้ Java เป็น Business Tier

Related link from Roti

Java Preferences

เป็น feature หนึ่งใน JDK1.4+ ที่อนุญาติให้เราเก็บ Application Preferences ของเราได้
โดยแต่ละ Platform ก็จะมีวิธีเก็บที่ต่างกัน
  • Windows เก็บลง Windows Registry
  • Unix เก็บลง File System
  • Mac Os X เก็บลง plist file ใน Library Folder


เราสามารถเลือก Preferences Store ได้ 2 แบบคือ
  • Per User
  • Per System


แต่ละแบบจะมีที่เก็บต่างกันไป
อย่างเช่นใน Unix
ที่เก็บของ Per User ก็จะอยู่ที่ ​~/.java/.userPrefs
แต่ถ้าเป็น Per System ก็ขึ้นอยู่กับว่า ถ้าที่ /etc มี directory .java
ก็จะไปสร้างที่ /etc/.java/.systemPrefs
แต่ถ้าไม่มี directory นั้น ก็จะไปใช้ ที่ /JAVA_HOME/.systemPrefs แทน

Note: ที่เก็บกรณี unix นี้สามารถ override ได้โดยใช้ System Properties
  • java.util.prefs.userRoot กำหนด root directory ของ user (default คือ user.home)
  • java.util.prefs.systemRoot กำหนด root directory ของ system

(ระวังด้วยว่า ถ้าเป็น IBM JDK หรือ Rockit JDK อาจจะไม่ใช่ค่านี้ก็ได้)

ส่วนใน OS X
ถ้าเป็น per User ก็จะเก็บใน ~/Library/Preferences
ถ้าเป็น per System ก็จะอยู่ใน /Library/Preferences
โดยจะประกอบด้วย 2 file ก็คือ file com.apple.java.util.prefs.plist
จะใช้เก็บ node path อย่างเดียว
แล้วก็ file plist ที่ใช้เก็บค่าจริงๆ อีก 1 file ต่อ 1 node (เฉพาะ leaf node)

การเก็บค่าลง Preferences สามารถใช้ Datatype ได้ดังนี้

  • String
  • boolean
  • ByteArray
  • double
  • float
  • int
  • long


กรณีที่ใช้ String หรือ ByteArray ต้องระวังข้อจำกัดในเรื่อง
ขนาดของ value ที่เก็บ โดยค่า maximum length ที่ยอมให้เก็บได้
ก็คือ 8192 characters

ส่วนข้อจำกัดอื่นๆ ก็คือ
ขนาดของ key ก็คือ ความยาวห้ามเกิน 80 characters
ขนาดของ node name ห้ามยาวเกิน 80 characters

Feature ที่น่าสนใจอีกอย่าง ก็คือ สามารถสั่ง Import และ
Export ข้อมูลใน preferences ได้

นอกจากนี้เราสามารถ Implements Preferences ของเราเองได้
โดย implements Interfaces PreferencesFactory
และบอกให้ vm รู้โดยการ set System Property -Djava.util.prefs.PreferencesFactory=my.PreferencesFactory

อ่านเพิ่มเติม

Related link from Roti

Sunday, August 21, 2005

Javascript 101

อ่านเจอ Idea ของ 'bact ในเรื่องการใช้ closure ในการ modify tree structure แล้ว
ก็เลยไปค้นหาดูบทความดีๆ ที่พูดเรื่อง closure พบบทความที่น่าสนใจก็คือ
Javascript Closures
หลังจากนั่นอ่่าน(ด้วยความทรมาน) อยู่พักใหญ่ ก็เลยเกิดแรงบันดาลใจว่า
เห็นควรจะได้ฤกษ์ทำความเข้าใจในเรื่อง Javascript ของตัวเองเสียที (หลีกเลี่ยงมานานแล้ว)

Note: ข้อควรระวัง. Javascript นี้อ้างอิง ECMAScript เป็นหลัก
ไม่ได้ดูเรื่อง browser compatiblilty เลย


Execution Context


Note: คำอธิบายนี้เป็น abstract
การ implement ของ Javascript Engine จริงๆ อาจจะมี object model
ซับซ้อนกว่านี้


Execution Context ก็คือ Object ธรรมดานี่แหล่ะ
เมื่อเราเริ่มต้น run javascript
เจ้า javascript engine ก็จะทำการ initialize "Global Execution Context"
ให้เรา โดยมี method (หรือ function) จำนวนหนึ่งมาให้ เช่น Date, encodeURI, isNAN, ...
(พวกนี้ก็คือ build in function ของ javascript นั่นเอง)

เมื่อไรก็ตามที่เรามีการ call function
เจ้า Javascript engine ก็จะทำการสร้าง Execution Context Object
ตัวใหม่ให้เรา และทำการ push current Execution Context ลงใน
Stack ก่อน เพื่อที่ว่าเมื่อมีการ return ออกจาก function แล้ว
ก็จะ pop เอา Execution Context ที่อยู่ใน Stack ขึ้นมาเป็น current Execution Context ต่อไป

ลองดูตัวอย่าง

1: function callMe() {
2: var x = 10;
3: }
4:
5: a = 'hi';
6: callMe();

เมื่อเริ่มต้น run Javascript นี้.
เจ้า Javascript Engine ก็จะทำการ
Initialize new Global Execution Context ขึ้นมา.

เมื่อ Engine อ่านเจอบรรทัดที่ 1 ที่เป็นการประกาศ function
ก็จะทำการ add function นี้เข้าเป็นสมาชิก (method)
ของ Global Execution Context

ส่วนที่บรรทัดที่ 5 ที่มีการ initialize variable.
variable a ตัวนี้จะถูกสร้างเป็นสมาชิก (property) ของ
current scope ซึ่งก็คือ Global Execution Context นั่นเอง

ที่บรรทัดที่ 6 มีการ call ไปที่ function callMe
JavaScript Engine ก็จะสร้าง Execution Context ใหม่ขึ้นมา,
จากนั้นก็ push Global Execution Context ลง stack ก่อน
สุดท้ายก็ set Execution Context ตัวใหม่นี้ เป็น current Execution Context

ที่บรรทัดที่ 2 มีการ initialize variable x
variable x จะถูกสร้างให้เป็นสมาชิกของ current
Execution Context และเมื่อจบจาก function callMe
current Execution Context ก็จะถูก Garbage ทิ้งไป (ตัวแปร x
ก็จะหายไป) และทำการ pop Execution Context จาก stack
ขึ้นมา set เป็น current Execution Context แทน

__proto__


ตัวนี้เป็น internal Object Reference ซึ่งชี้ไปยัง Object ที่เราต้องการให้เป็นต้นแบบ
(ทุก Object ภายใน javascript จะมี property __proto__ เสมอ)
ดูตัวอย่างดีกว่า เข้าใจง่ายกว่า

1 : function Fruit() {
2 : this.eatable = true;
3 : }

4 : function Apple() {
5 : this.color = "red";
6 : }

7 : Apple.prototype = new Fruit();
8 : var apple1 = new Apple();

9 : print (apple1.color); // ==> red
10: print (apple1.eatable); // ==> true
11: print (apple1.x);

ที่บรรทัดที่ 7 เรากำหนดให้ prototype property
ของ Apple Function ไห้ชี้ไปยัง Object ที่สร้างขึ้นมาจาก Object Fruit

เมื่อไรก็ตามที่เราเรียกมีการ new Apple()
Instance ที่สร้างขึ้นใหม่นี้จะถูก set ค่า __proto__ ให้ชี้ไปยัง
ค่าที่กำหนดไว้ใน prototype property

ที่นี้คำถาม ก็คือ ทำไมต้องมี __proto__
__proto__ จะถูกใช้เมื่อเรามีการ access properties ของ Object นั้น
แล้ว JavaScript Engine ไม่สามารถหา property ชื่อที่เราต้องการใน Object นั้นได้
เจ้า Javascript engine ก็จะขยับออกไปหา property นั้นจาก
Object ที่ __proto__ ชี้อยู่แทน ถ้าหาไม่เจออีก ก็ขยับขึ้นไปอีก
จนกว่า จะเจอค่า __proto__ ที่เป็น null

จากข้างบนตัวอย่างข้างบน เราสามารถเขียนแบบกำหนด __proto__
ได้ตรงๆดังนี้

function Fruit() {
this.eatable = true;
}

function Apple() {
this.color = 'red';
}
y = new Apple();
z = new Fruit();
y.__proto__ = z;

print (y.color);
print (y.eatable);


ข้อควรระวังในการใช้ Inner Function


จากตัวอย่างนี้

function Position(x, y) {
this.dump = function() {
println ("x = " + x + ", y = " + y);
}
}

p = new Position(10, 20);
p.dump();

การที่เราประกาศแบบนี้ ทุกครั้งที่เรา new Position ขึ้นมา
จะเกิด Object dump ขึ้นมาด้วย ถ้ามี Position 100 instance
ก็จะมี Object dump 100 instance ด้วยเช่นกัน

ดังนั้นเป็นการดีกว่าที่จะกำหนดเป็น prototype แทนเพื่อให้ลดการใช้ memory ลง

function Position(x, y) {
this.x = x;
this.y = y;
}

Position.prototype.dump = function () {
println ("x = " + this.x + ", y = " + this.y);
}

p = new Position(10, 20);
p.dump();


ปัญหาอีกอย่างของ inner function ก็คือ
memory leak

function add(x) {
return function innerAdd(y) {
return x + y;
}
}

a = add(10);
println (a(2)); // ==> 12

การที่เรากำหนด function add ไว้อย่างนี้
ทำให้เกิดปัญหา memory leak ได้
เนื่องจากในขณะที่เรา new function innerAdd
ขึ้นมานั้น innerAdd จะมี reference ไปยัง
กลุ่มของ object ที่อยู่ใน execution context จำนวนหนึ่ง

และเมื่อมีการ return ค่ากลับออกมา
Garbage ก็จะพยายาม release resources
ที่ถูกจองใน Execution Context ของ funcion add
แต่ก็ไม่สามารถ release ได้ (เพราะยังมี pointer จาก a อ้างถึงอยู่)
วิธีที่ดีกว่า ก็คือ

function add(x) {
this.x = x;
return addAgain;
}

function addAgain(y) {
return this.x + y;
}

a = add(10);
println (a(2)); // ==> 12

Related link from Roti