Friday, August 19, 2005

ใช้ Spring AOP ในการ initialize Hibernate Lazy Properties

ปัญหาของการใช้ Hibernate ตัวหนึ่งที่เชื่อว่าทุกคนคงจะเคยเจอ (โดยเฉพาะพวกที่
พัฒนา Web Application) ก็คือ ประเด็นเรื่อง LazyInitializationException
ซึ่งเกิดจากการพยายาม access Lazy Properties หลังจากที่ได้มีการ
close Session ไปแล้ว

สภาพปัญหา
สมมติว่าเราออกแบบ Class Customer กับ Class CustomerHistory
โดย CustomerHistory ต้องการจะไว้ใช้เก็บประวัติการซื้อของลูกค้า



เวลาเรา Mapping เข้ากับ Hibernate
Navigation จาก customer ไปยัง CustomerHistory จะ config ได้ดังนี้
<set
name="customerHistory"
lazy="false"
cascade="none"
sort="unsorted"
order-by="invoiceDate"
>

<key
column="customer_id"
>
</key>
<one-to-many
class="domain.CustomerHistory"
/>

</set>

จะเห็นว่าใน code ตัวอย่างนี้มี lazy="false"

เมื่อไรก็ตามที่สั่ง query Customer นี้เช่น
List results = session.createQuery("from Customer as c").list();

Note:สำหรับผู้ที่สงสัยว่าทำไมไม่เขียน session.find("from Customer...")
เพราะว่าใน Hibernate3 Class Session ไม่มี method find ให้ใช้แล้ว


Hibernate ก็จะจัดการ query โดยการ
select * from Customer
จากนั้น แต่ละ Customer ที่ได้มา
select * from customer_history

ซึ่งจะเห็นว่าถ้าเอาไปใช้งานในหน้าจอประเภทสอบถามที่ดึง customer ทีเดียวหลายๆคน
หรือหน้าจอรายงาน ก็จะเกิดปัญหาในด้าน Performance Issue แน่นอน

ดังนั้นเรามักจะกำหนดให้ lazy="true" เสียมากกว่า
ซึ่งเมื่อกำหนดเช่นนี้แล้ว Hibernate จะยังไม่ select ข้อมูลจาก customer_history
จนกว่าจะมีการ access จริงเกิดขึ้น (เช่นมีการเรียกใช้ customer.getCustomerHistory().size())
ซึ่งการ set lazy="true" นี่แหละที่เป็นที่มาของปัญหา
LazyInitializeException

ดังนั้นในกรณีที่เรารู้ว่า Object เราจะถูกใช้หลังจากที่ Session close แล้ว
และมีการ access lazy Properties ด้วย. Hibernate เปิดโอกาสให้เรา
initialize Lazy Property ด้วยคำสั่ง Hibernate.initialize(..)

ในแง่การปฎิบัติแล้ว วิธี Hibernate.initialize ก็สามารถใช้การได้ดี
แต่ในแง่ความสวยงามของ Design แล้วดูมันผิดที่ิผิดทางอยู่
สุดท้ายพวกคนที่ใช้ Spring ก็เลยมี Idea ที่จะแก้ไขปัญหานี้ด้วยการใช้
Spring AOP เข้ามาช่วย ​(เพื่อให้การกำหนด initialize property
มีลักษณะเป็น Declarative มากขึ้น)

ลองมาดูวิธีการที่เขาใช้
เริ่มจากสถานะการณ์ปกติที่เราใช้ Spring ก่อน

define Service Layer ด้วย Interface
public interface ICustomerServices {    
public List findAllCustomer();
}


ใน config file เรา declare CustomerService โดยใช้ Proxy
เพื่อจะได้ใส่ hibernate interceptor ที่ทำหน้าที่ open, close Session
ให้เราอัตโนมัติ
<bean id="myHibernateInterceptor" 
class="org.springframework.orm.hibernate3.HibernateInterceptor">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>

<bean id="customerServiceTarget" class="service.CustomerService">
<property name="sessionFactory">
<ref bean="mySessionFactory"/>
</property>
</bean>

<bean id="customerService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>service.ICustomerServices</value>
</property>
<property name="interceptorNames">
<list>
<value>myHibernateInterceptor</value>
<value>customerServiceTarget</value>
</list>
</property>
</bean>


เมื่อไรก็ตามที่เราต้องการ Initialize Properties เราก็สามารถ
แทรก Interceptor เข้าไปได้ดังนี้
<bean id="cust-service-findall_initialize" 
class="org.springmodules.aop.framework.TouchingNameMatchMethodAdvisor">
<property name="mappedNames">
<value>findAllCustomer</value>
</property>
<property name="advice.ognl">
<list>
<value>#returned.{customerHistory.size}</value>
</list>
</property>
</bean>

และทำการแทรก Interceptor เข้าไปใน proxy ดังนี้
<bean id="customerService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>service.ICustomerServices</value>
</property>
<property name="interceptorNames">
<list>
<value>myHibernateInterceptor</value>
<value>cust-service-findall_initialize</value>
<value>customerServiceTarget</value>
</list>
</property>
</bean>

Class TouchingNameMatchMethodAdvisor จะคอยดักว่ามีการ เรียกใช้ method
ที่กำหนดหรือไม่ ถ้ามีการเรียกใช้ ก็จะใช้ OGNL Expression ที่เรากำหนด
เข้าไป access property

สรุป
ในแง่ Design การยกเอา Initilize code ออกมาเป็น Declarative ดู
มี style กว่า แต่ในแง่ปฏิบัติและความยุ่งยากแล้ว อาจจะไม่คุ้มกันก็ได้
เพราะถ้าเขียนเป็น code แล้วจะมี code แค่เพียง 1 บรรทัด แต่ถ้ายกออกมา
เป็น declarative แล้วต้องเขียน code ขึ้นอีก 12 บรรทัด

Note: Class TouchingNameMatchMethodAdvisor อยู่ใน Project SpringModules
และยังไม่รวมอยู่ใน release build ของ SpringModules



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

Related link from Roti

Thursday, August 18, 2005

Spring Package ทำพิษ

วันนี้จะทดสอบ Spring Function ใหม่ๆ ก็เลย setup project
ใน eclipse กะว่าจะลอง Hibernate3
ได้เรื่องเลย แค่ buildSessionFactory ก็เจอ exception
AbstractMethodError ในส่วนของการ getDataMajorVersion ใน postgres jdbc
นั่งดูอยู่ต่างนาน เพราะว่าเฉพาะตัว hibernate3 ก็เคย test แล้ว
มันทำงานได้นี่หว่า
สุดท้าย ก็เจอว่าเป็น bug ตัวนี้ SPR-1185
ทางแก้ก็คือ อย่าใช้ hibernate3.jar ที่มากับ spring

Note: อย่าไว้ใจ jar ที่ได้มาง่ายๆ

Related link from Roti

Tuesday, August 16, 2005

SpellChecker with Lucene

SpellChecker เป็น API extension ของ lucene ช่วยในการ implement feature
ที่ใช้ในการตรวจสอบ word ที่เข้ามาว่าถูกต้องตรงใน dictionary หรือไม่
กรณีไม่ถูก ก็สามารถ suggest คำที่ไกล้เคียงที่สุดให้ได้ด้วย

โดยส่วนใหญ่เรามักจะใช้ algorithm minimum edit distance
ในการ solve หาคำที่ไกล้เคียงที่สุด แต่ใน SpellChecker นี้
ใช้ technique ที่เรียกว่า n-gram

การใช้งาน SpellChecker จะเริ่มต้นด้วยการ create Index ที่จะใช้ search ก่อน
โดย index นี้ใช้ api ของ lucene ในการสร้างขึ้นมา

ส่วนข้อมูลที่จะป้อนเข้าไปเก็บใน index นี้จะต้องป้อนผ่าน
Dictionary ซึ่งทาง SpellChecker เตรียม Dictionary
มาให้เรา 2 แบบคือ
  • PlainTextDictionary
    ใช้กับ input word ที่อยู่ในรูป Text File
  • LuceneDictionary
    ใช้กรณีที่เราต้องการ extract word มาจาก lucene index ที่มีอยู่แล้ว


ตัวอย่างการสร้าง index
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.spell.Dictionary;
import org.apache.lucene.search.spell.LuceneDictionary;
import org.apache.lucene.search.spell.SpellChecker;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

...

IndexReader reader = null;
try {

Directory indexdir = FSDirectory.getDirectory("/tmp/luceneIndex", false);
Directory spellIndex = FSDirectory.getDirectory("/tmp/spellChecker", true);

reader = IndexReader.open(indexdir);
Dictionary dict = new LuceneDictionary(reader, "name");
SpellChecker spellChecker = new SpellChecker(spellIndex);
spellChecker.indexDictionnary(dict);

} finally {
if (reader != null) {
reader.close();
}
}


ข้อมูลที่เก็บไว้ใน index จะแยกเก็บเป็น 1 document(lucene document)
ต่อ 1 word ที่ป้อนเข้าไป
สมมติว่าเรามี word "พระโขนง" เจ้า SpellChecker จะเก็บเป็น index ดังนี้



เห็นได้ว่ามีการใช้ 3-gram กับ 4-gram เป็นหลักเนื่องจากคำนี้มีความยาวมากพอ
โดยจะแบ่งออกเป็น 4 ส่วนคือ start, gram, end, word

ส่วนกรณีการ search หา ก็สามารถทำได้ดังนี้
กรณีที่ตรวจว่ามีคำนี้ใน Spell Dict. ของเราหรือไม่ ก็ใช้คำสั่งนี้
Directory spellIndex = FSDirectory.getDirectory("/tmp/spell", false);       
SpellChecker spell= new SpellChecker(spellIndex);
spell.exist("พระโขนง");

Note: SpellCheck จะใช้ method ของ lucene ที่ชื่อ docFreq
ในการ search แบบนี้

ส่วนกรณีที่ต้องการหา word ที่คล้ายกันก็ใช้คำสั่งนี้
(เลข 2 คือ parameter ที่บอกว่าต้องการ similar word แค่ 2 ตัวพอ)
String[] strs = spell.suggestSimilar("พระโขง", 2);

results:
พระโขนง
พระแสง



ขั้นตอนการทำงานภายใน ของ suggestSimilar
จาก word ที่เข้ามา SpellChecker จะจัดการสร้าง Boolean Query ที่มี query แบบนี้
start3:พระ^2.0
gram3:ระโ
gram3:ะโข
end3:โขง
start4:พระโ^2.0
gram4:ระโข
end4:ะโขง

จะเห็นได้ว่า startN จะถูก boost ด้วย 2

ถ้าลองดูผลลัพท์ที่ได้ จะเห็นว่าได้มาเยอะเชียว (ตัวอย่างข้อมูลที่ใช้เป็น dictionary
ของ อำเภอทั่วประเทศ)



จากนั้นผลการ search ที่ได้ จะถูกคำนวณ score ด้วย algorithm Levenshtein distance
และเรียงลำดับก่อน return กลับมาให้ผู้ใช้

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

Related link from Roti

Monday, August 15, 2005

แสงอุ่นๆ



ROCKWELL KENT
Baker of the Bread of Abundance, 1945
The Baker of the Bread of Abundance, which appears to illustrate its own story, showcases Kent’s propensity toward strong, contrasting light and color. Here, the family dining at the table is awash in the warm illumination that radiates from the loaf of bread. In the large hands of the looming figure, Kent may have intended the bread to symbolize American abundance and family togetherness at the end of World War II.

Related link from Roti

Sunday, August 14, 2005

ข้อควรระวังในการ set Gregorian Calendar

ใช้มาตั้งนานพึ่งรู้ว่ามีโอกาศ set ผิดได้ ลองดูตัวอย่างนี้
เราสามารถ set GregorianCalendar ได้ 2 แบบ คือ
ผ่านทาง Constructor หรือ ผ่านทาง method set
ของมัน
GregorianCalendar cal1 = new GregorianCalendar(2005, Calendar.AUGUST, 12);
GregorianCalendar cal2 = new GregorianCalendar(2005, Calendar.AUGUST, 12, 0, 0,
0);
GregorianCalendar cal3 = new GregorianCalendar();
cal3.set(Calendar.MILLISECOND, 0);
cal3.set(Calendar.SECOND, 0);
cal3.set(Calendar.MINUTE, 0);
cal3.set(Calendar.HOUR, 0);
cal3.set(Calendar.YEAR, 2005);
cal3.set(Calendar.MONTH, Calendar.AUGUST);
cal3.set(Calendar.DATE, 12);

ค่า TimeInMillis ของ cal1 กับ cal2 จะมีค่าตรงกัน
ส่วนค่าของ cal3 มีโอกาศผิดได้ ขึ้นอยู่กับว่าเรา
run program นี้ตอนกี่โมง ถ้าเรา run ตอนเข้า ค่า cal3
ก็จะถูกต้อง แต่ถ้าเรา run ช่วงบ่ายค่า cal3 ก็จะไม่ถูกต้อง
(คำว่าถูกต้องหมายถึง ค่าที่ได้จาก method getTimeInMillis
ของ cal3 มีค่าตรงกับ cal2.getTimeInMillis())

ที่เป็นเช่นนี้เพราะว่า algorithm ในการคำนวณ timeInMillis
ของ GregorianCalendar มันจะคำนวณก่อนว่าเรามีการ
set ค่า HOUR_OF_DAY หรือ HOUR
ถ้า set ค่า HOUR มันจะนำ AM_PM value มาคำนวณด้วย
ดังนั้นในกรณีเรา run ช่วงบ่าย ตอนที่เรา new GregorianCalendar()
เราจะได้ AM_PM = Calendar.PM ทำให้ค่า HOUR ที่เรา set
เป็น 0 นั้นหมายถึงตอน 12.00 นาฬิกา
// from GregorianCalendar.java
int hourOfDayStamp = stamp[HOUR_OF_DAY];
int hourStamp = stamp[HOUR];
int bestStamp = (hourStamp > hourOfDayStamp) ? hourStamp : hourOfDayStamp;
// Hours
if (bestStamp != UNSET) {
if (bestStamp == hourOfDayStamp) {
// Don't normalize here; let overflow bump into the next period.
// This is consistent with how we handle other fields.
millisInDay += internalGet(HOUR_OF_DAY);
fieldMask |= 1 << HOUR_OF_DAY;
} else {
// Don't normalize here; let overflow bump into the next period.
// This is consistent with how we handle other fields.
millisInDay += internalGet(HOUR);
fieldMask |= 1 << HOUR;

// The default value of AM_PM is 0 which designates AM.
if (stamp[AM_PM] != UNSET) {
millisInDay += 12 * internalGet(AM_PM);
fieldMask |= 1 << AM_PM;
}
}
}

ดังนั้นในกรณีที่ต้องการ set ให้ถูกต้อง ก็ต้องเลือก set
โดยใช้ HOUR_OF_DAY หรือ HOUR
ในกรณีใช้ HOUR ก็ต้องทำการ set AM_PM ด้วย
// use HOUR_OF_DAY
cal3.set(Calendar.HOUR_OF_DAY, 0);

// use HOUR & AM_PM
cal3.set(Calendar.HOUR, 0);
cal3.set(Calendar.AM_PM, Calendar.AM);

Related link from Roti