Posts Tagged ‘DynamicBlock’

Dynamic Block ของ iBatis ภาคขยายครับ

January 26th, 2009

เขียนภาคสามเสร็จแล้วแต่พอดีเหลือบไปเห็นคำถามเรื่อง 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 เดียวครับ
พอไหวไหมเอ่ย ==”