เขียนภาคสามเสร็จแล้วแต่พอดีเหลือบไปเห็นคำถามเรื่อง Dynamic Block ของ iBatis ครับเลยเขียนตอบเป็นเรื่องเป็นราวเลยละกันครับ
จริงๆตัวผมเองถึงแม้จะใช้ iBaits มานานสักระยะแต่ก็ไม่เคยใช้ Dynamic Block เลยเพราะขี้เกียจทั้งๆที่จริงๆแล้ว iBator เค้าก็สร้างออก
มาให้ตลอดนะ ผมก็ลบทิ้งเช็ด จนช่วงปีหลังนี่แหละผมได้แนะนำน้องที่รู้จักกันให้ใช้ iBatis เพราะงานของน้องเค้าคือต้องเขียน Application
ครอบฐานข้อมูลเดิมที่ถูกออกแบบไว้แล้วโดย DBA ดังนั้นให้ใช้ Hibernate ท่าจะแย่เลยเพราะคงต้องรื้ออะไรบางอย่างหวยเลยออก iBatis ซะ
อยู่มาวันหนึ่งก็เอา code น้องมาดู “เอ้ยมันทำแบบนี้ได้ด้วยหรอ” นั่นแหละถึงเห็นว่า Dynamic Block หรือ Example Class ให้อย่างไร
ติ๊ต่างว่าเราใช้ตารางเดิมครับที่มี Schema ดังนี้
CREATE TABLE /*!32312 IF NOT EXISTS*/ `users` ( `USR_ID` tinyint(3) unsigned NOT NULL DEFAULT '0', `USR_NAME` varchar(50) NOT NULL DEFAULT '', `USR_PWD` varchar(50) DEFAULT NULL, `USR_LEVEL` int(10) unsigned DEFAULT NULL, `USR_FIRST_LOGIN` date DEFAULT NULL, `REG_DATE` date NOT NULL, `LOG_DATE` date DEFAULT NULL, PRIMARY KEY (`USR_NAME`) ) ENGINE=InnoDB /*!40100 DEFAULT CHARSET=utf8*/;
ในทางปฏิบัติจริงผมอยากสร้าง Query ที่ออกมาหน้าตาแบบนี้ครับ
SELECT * FROM USERS WHERE USR_ID < 20 AND USR_NAME LIKE '%abc%' AND USR_LEVEL > 3;
และอย่างที่เรารู้กันดีว่าหัวใจหลักของ iBatis คือการทำ XML Map ดังนั้นทางออกสำหรับ solution นี้แบบบ้านๆที่สุดคือสร้าง map ทุกๆ Queryใหม่
ที่เราต้องาการใช้ยกตัวอย่างเช่นเราสามารถสร้าง mapping ดังนี้ได้
<select id="Users_selectByPrimaryKey" resultMap="UsersResult" parameterClass="com.spring66.tutorial.model.Users" >
select USR_ID, USR_NAME, USR_PWD, USR_LEVEL, USR_FIRST_LOGIN, REG_DATE, LOG_DATE
from users
where USR_ID < #usrId:CHAR#
AND USR_NAME LIKE #usrName:VARCHAR#
AND USR_LEVEL > #usrLevel:INTEGER#
</select>
ทางออกนี้โอเคไม่มีปัญหาทำงานได้แต่เราต้องตรวจสอบค่าของพารามิเตอร์เองว่าเป็น NULL หรือไม่ และถ้าเกิดมี Query แบบนี้ตามมาอีกสิบแบบทำไง?
ตอบง่ายๆก็สร้าง mapping อีกสิบอันก็จบข่าวแต่ ประเด็นคือมันเยอะไปไหม iBatis เลยจัดทางออกอีกทางเรียกว่า Dynamic Query อันนี้เริ่มเข้าท่าโดย
Dynamic Query คือการสร้าง SELECT Block ที่มีเงื่อนไขในมาดังนี้
<select id="dynamicGetUsersList" resultMap="UsersResult" > SELECT * FROM USERS <dynamic prepend="WHERE"> <isLessThan prepend="AND" property="usrId"> USR_ID = #usrId# </isLessThan> <isLike prepend="AND" property="usrName"> USR_NAME like #usrName# </isLike> <isGreaterThan prepend="AND" property="usrLevel"> USR_LEVEL = #usrLevel# </isGreaterThan> </dynamic> </select>
วิธีนี้หล่อขึ้นมาหน่อยคือเราสามารถส่งข้อมูลมาไม่ครบได้เพราะ iBatis จะทำการตรวจสอบเองว่าพารามิเตอร์ตัวไหนเป็น NULL หรือไม่ถ้า NULL
เงื่อนไขนั้นๆก็จะไม่ถูกสร้าง แต่อย่างไรก็ตามวิธีนี้ก็ยัง ยังไม่ Dynamic มากๆเพราะเราใส่เงือนไขได้มากสุดสามอัน มากกว่านี้ไม่ได้
ถ้าต้องการสร้างให้ได้มากว่านี้ iBatis เตรียม Dynamic Block ไว้ให้ครับโดยที่เราจะต้องสร้างคลาสใหม่ขึ้นมากก่อนเพื่อใช้บรรจุเงื่อนไขทั้งหมดที่เราต้อง
การโดยที่นี้ใช้ชื่อ UsersExpression ครับมีรายละเอียดดังนี้(ตัดออกไปบ้างแล้วครับ เพราะยาวมากๆ)
package com.spring66.tutorial.model;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class UsersExpression {
protected String orderByClause;
protected List oredCriteria;
public UsersExpression() {
oredCriteria = new ArrayList();
}
protected UsersExpression(UsersExpression example) {
this.orderByClause = example.orderByClause;
this.oredCriteria = example.oredCriteria;
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public List getOredCriteria() {
return oredCriteria;
}
public void or(Criteria criteria) {
oredCriteria.add(criteria);
}
public Criteria createCriteria() {
Criteria criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected Criteria createCriteriaInternal() {
Criteria criteria = new Criteria();
return criteria;
}
public void clear() {
oredCriteria.clear();
}
public static class Criteria {
protected List criteriaWithoutValue;
protected List criteriaWithSingleValue;
protected List criteriaWithListValue;
protected List criteriaWithBetweenValue;
protected Criteria() {
super();
criteriaWithoutValue = new ArrayList();
criteriaWithSingleValue = new ArrayList();
criteriaWithListValue = new ArrayList();
criteriaWithBetweenValue = new ArrayList();
}
public boolean isValid() {
return criteriaWithoutValue.size() > 0
|| criteriaWithSingleValue.size() > 0
|| criteriaWithListValue.size() > 0
|| criteriaWithBetweenValue.size() > 0;
}
public List getCriteriaWithoutValue() {
return criteriaWithoutValue;
}
public List getCriteriaWithSingleValue() {
return criteriaWithSingleValue;
}
public List getCriteriaWithListValue() {
return criteriaWithListValue;
}
public List getCriteriaWithBetweenValue() {
return criteriaWithBetweenValue;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteriaWithoutValue.add(condition);
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
Map map = new HashMap();
map.put("condition", condition);
map.put("value", value);
criteriaWithSingleValue.add(map);
}
protected void addCriterion(String condition, List values, String property) {
if (values == null || values.size() == 0) {
throw new RuntimeException("Value list for " + property + " cannot be null or empty");
}
Map map = new HashMap();
map.put("condition", condition);
map.put("values", values);
criteriaWithListValue.add(map);
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
List list = new ArrayList();
list.add(value1);
list.add(value2);
Map map = new HashMap();
map.put("condition", condition);
map.put("values", list);
criteriaWithBetweenValue.add(map);
}
public Criteria andUsrIdGreaterThan(String value) {
addCriterion("USR_ID >", value, "usrId");
return this;
}
public Criteria andUsrIdLessThan(String value) {
addCriterion("USR_ID <", value, "usrId");
return this;
}
public Criteria andUsrNameIsNull() {
addCriterion("USR_NAME is null");
return this;
}
public Criteria andUsrNameIsNotNull() {
addCriterion("USR_NAME is not null");
return this;
}
public Criteria andUsrNameEqualTo(String value) {
addCriterion("USR_NAME =", value, "usrName");
return this;
}
public Criteria andUsrNameNotEqualTo(String value) {
addCriterion("USR_NAME <>", value, "usrName");
return this;
}
public Criteria andUsrNameGreaterThan(String value) {
addCriterion("USR_NAME >", value, "usrName");
return this;
}
public Criteria andUsrNameGreaterThanOrEqualTo(String value) {
addCriterion("USR_NAME >=", value, "usrName");
return this;
}
public Criteria andUsrNameLessThan(String value) {
addCriterion("USR_NAME <", value, "usrName");
return this;
}
public Criteria andUsrNameLessThanOrEqualTo(String value) {
addCriterion("USR_NAME <=", value, "usrName");
return this;
}
public Criteria andUsrNameLike(String value) {
addCriterion("USR_NAME like", value, "usrName");
return this;
}
public Criteria andUsrLevelGreaterThan(Integer value) {
addCriterion("USR_LEVEL >", value, "usrLevel");
return this;
}
public Criteria andUsrLevelLessThan(Integer value) {
addCriterion("USR_LEVEL <", value, "usrLevel");
return this;
}
}
}
}
ยาวมากแต่ไม่ต้องกลัวครับ iBator ช่วยท่านได้ เอ่าจากนั้นเราจะต้องบอก iBatis ว่าเราจะใช้ Dynamic Block โดยการสร้าง block ไว้ใน users.xml ครับ
<sql id="Where_Clause" >
<iterate property="oredCriteria" conjunction="or" prepend="where" removeFirstPrepend="iterate" >
<isEqual property="oredCriteria[].valid" compareValue="true" >
(
<iterate prepend="and" property="oredCriteria[].criteriaWithoutValue" conjunction="and" >
$oredCriteria[].criteriaWithoutValue[]$
</iterate>
<iterate prepend="and" property="oredCriteria[].criteriaWithSingleValue" conjunction="and" >
$oredCriteria[].criteriaWithSingleValue[].condition$
#oredCriteria[].criteriaWithSingleValue[].value#
</iterate>
<iterate prepend="and" property="oredCriteria[].criteriaWithListValue" conjunction="and" >
$oredCriteria[].criteriaWithListValue[].condition$
<iterate property="oredCriteria[].criteriaWithListValue[].values" open="(" close=")" conjunction="," >
#oredCriteria[].criteriaWithListValue[].values[]#
</iterate>
</iterate>
<iterate prepend="and" property="oredCriteria[].criteriaWithBetweenValue" conjunction="and" >
$oredCriteria[].criteriaWithBetweenValue[].condition$
#oredCriteria[].criteriaWithBetweenValue[].values[0]# and
#oredCriteria[].criteriaWithBetweenValue[].values[1]#
</iterate>
)
</isEqual>
</iterate>
</sql>
เอาละเกือบเส็ดละครับต่อไปเราจะใช้งานมันยังไงล่ะครับโดยเราสามารถเขียน Logic ในการสร้าง Query ดังนี้ครับ
usrEx.createCriteria().andUsrIdLessThan(20).andUsrNameLike("%us%").andUsrIdGreaterThan(5);
List<Users> usr = usersDao.selectByExpression(usrEx);
เพียงเท่านี้ iBatis หลังจากได้รับ Expression มาแล้วก็จะไปวนไปวนมาอยู่ใน Where_Clause Block ครับว่าส่งอะไรเข้ามาบ้าง
และจะสร้าง SQL ออกมาได้อย่างไรบ้างนี่แหละครับจะเห็นได้ว่าเรามี XML Mapping แค่เพียง block เดียวครับ
พอไหวไหมเอ่ย ==”

แนวคิดเดียวกับ Dynamic Finder ใน GORM เลย อิอิอิ ถ้าได้ DSL มาสักชุดคงจะช่วยลด coding ไปได้เยอะครับ
เข้าใจแล้วครับ ขอบคุณมากๆครับ
ยาวๆอย่างนี้ ต้องเปลี่ยนไปใช้ groovy builder แทน