Archive for January, 2009

Introducing Spring Batch แบบ ผิวๆ บางๆ

January 30th, 2009

เรื่องของเรื่อง ก็มาจาก ที่ว่า คุณ roofimon อยากได้ โจทย์มาทำ dojo ครับเลย อยากได้ sample data ใน database ซัก แสน record เลยใช้อำนาจมืด(เอ้ย!)ไหว้วานให้ผมทำ
ก็เลย มานั่งนึกว่าเอ้อ จะเขียนแบบธรรมดาๆ ก็ ดูว่า จะเสียชื่ออาจารย์อย่างคุณ roofimon ก็เลย คิดแบบเมพๆ หน่อยเขียน spring batch ดีกว่า ก็เลย เริ่มกันไปเลย เริ่มกันที่ intro กันก่อนแล้วกัน

ตาม entry เรื่อง Spring Product ของคุณ roofimon เรื่อง spring และลูกๆ spring batch ก็คือหนึ่งในลูกๆ ของ spring ตัวแม่ ในความเห็นผมเอง มองว่า ประโยชน์ของการใช้งาน
spring batch นั่นมีส่วนที่คล้ายกับ spring aop ตัวเก่ง คือ มุ่งหมายจะทำใำห้ระบบทำ logic อันใดอันหนึ่ง โดย design แยกออกมาจาก main business logic โดย spring aop
นั้นมุ่งหมายให้ทำ โดยสามารถกำหนด ว่าจะให้ทำ หลัง, ก่อน หรือ ทั้งก่อนหลัง การกระทำสิ่งใดอีกสิ่งหนึ่ง(main business logic) ทุกครั้งเสมอเฮ้อ เขียนแล้วงง เอง แต่ spring batch ก็ คือ สิ่งที่อยากให้ทำ ในเวลาหรือว่าจังหว่ะที่กำหนด
ที่เจอกันบ่อย ๆ เช่น ทุกวันศุกย์เย็น ต้อง import data from xml อะไรเทือกนั้นเป็นต้น
เกือบจะหมดแรงข้าวต้มละ ยังไมไ่ด้ขึ้นเลย อ่ะจะเข้าละ เด่วจะยืดเยื้อไปกันใหญ่ ผมคิดว่าการที่เราจะเรียนรู้และเข้าใจสิ่งใหม่ที่เราไม่เคยใช้ ง่ายที่สุดก็คือการทำตามตัวอย่าง งั้นก็อย่างที่บอก เรามาลองทำอะไรเล่นๆ ตามธรรมเนียมกัน ไหนๆก็ไหนๆ ก็เลย เขียนต่อ จาก spring66-app แล้วกันเพราะคาดว่าหลายๆ ท่านก็คงทำตามมาตลอด จัดไปครับ แบบผิวๆ บางๆ

ตัวละครของ Spring Batch
เรามาเริ่มกันที่ การแนะนำตัวละครหลักเลยดีกว่ครับ ซึ่งก็คือ ผู้มีบทบาทในการ เริ่มต้นใช้งานมันนั่นเอง มีดังนี้นะครับ ขอแนะนำแบบ ผิวๆ บางๆ นะครับ ขอคนช่วยเสริมเติมแต่งส่วนที่ผมเข้าใจผิดนะครับ
Job ก็คืองานหรือว่า batch นึงที่เราจะสร้างขึ้นครับ โดยในแต่ละงานอาจจะ ประกอบไปด้วยหนึ่งหรือหลายขั้นตอน(Step)ครับ Step ก็ คือขั้นตอนที่อยู่ใน Job นั่นหล่ะครับโดย Job ก็จะมี Instance คือ JobInstance ที่สามารถรับพาามิเตอร์ได้คือ JobParameter โดยในแต่ละการแสดงบทบาทของ JobInstance จะ ถูกเรียกว่า JobExecution เรา ลองนึกภาพเล่นๆ ว่า สมมุติเราจะทำการ นำเข้าข้อมูล users จำนวน นึงแล้วต้องการ ที่จะทำการ truncate ข้อมูลเก่าทิ้งทั้งหมด (อย่าถามนะครับว่าทำ ทำไม บอกแล้วว่าสมมุติ) งานที่เราจะทำก็จะมี สองขั้นตอนคือ หนึ่งทำการ truncate ก่อนแล้วแค่ทำการ insert ข้อมูลใหม่ลงไป พอเห็นภาพมั้ยครับ แต่ spring66 เราจะทำ toturial ทั้งทีใหญ่มหาศาลครับ จะมีงานเดียวได้อย่างไร เราจึงลืมไปไมไ่ด้เลยที่จะแนะนำ สิ่งที่ พอ spring batch เค้าต้องการอย่างแรงคือ JobRepository เป็นตัวเก็บ งานที่เราจะทำั้งหมดไว้ นั่นเองตรงนี้ไม่ค่อยเคลีย ในการอธิบายครับต้องรบกวนท่านอื่นเสริม แต่อ่ะ พอๆ เด่วจะตัวละครเยอะไปกันใหญ่ เอาหล่ะทีนี้ จากกระทู้ก่อนๆ เราก็มีการจัดเตรียมอุปกรณ์การแสดงเพียบ เราจึงไม่ต้องไปทำใหม่ทั้งหมด ขอไปเอาของเค้ามาใช้หน่อยแล้วกันนะครับ

เริ่มกันที่ xml ตัว พ่อครับ pom.xml นั่นเอง ถ้าผ่านมาทั้งหมดก็ น่าจะประมาณแบบนี้เนอะ


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.spring66.tutorial</groupId>
	<artifactId>spring66-app</artifactId>
	<packaging>jar</packaging>
	<version>1.0-SNAPSHOT</version>
	<name>spring66-app</name>
	<url>http://maven.apache.org</url>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.0.2</version>
				<configuration>
					<source>1.5</source>
					<target>1.5</target>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
			<plugin>
				<artifactId>maven-resources-plugin</artifactId>
				<version>2.2</version>
				<configuration>
					<encoding>UTF-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>3.8.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring</artifactId>
			<version>2.5.6</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>2.5.6</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.6</version>
		</dependency>
		<dependency>
			<groupId>commons-dbcp</groupId>
			<artifactId>commons-dbcp</artifactId>
			<version>1.2</version>
		</dependency>
	</dependencies>
</project>

เราก็ทำการเพิ่มสิ่งที่เราจะใช้เข้าไปโลดครับ


<dependency>

	<groupId>org.springframework.batch</groupId>

	<artifactId>spring-batch-core</artifactId>

	<version>1.1.3.RELEASE-A</version>

</dependency>

<dependency>

	<groupId>org.springframework.batch</groupId>

	<artifactId>spring-batch-infrastructure
	</artifactId>

	<version>1.1.3.RELEASE-A</version>

</dependency>

เรื่องของ dependency เนี่ย รบกวนต้องตามอ่านกันเอานะครับ เยอะเหลือหลายครับ blog แรกผมครับพลังน้อย

เหมือน อย่างเคยๆ เจ้าพ่อสปริงเค้าก็เขียนอะไรๆ มาให้ใช้เยอะแยะไปหมด เริ่มกันที่ เราจะทำการสร้าง csv file เก็บ รายชื่อของ usrId,usrName,usrPassword นะครับโดยใช้  , เป็นตัวขั้นหน้าตา ก็ประมาณนี้ users.csv

userId,userName,userPassword
id1,Poorprogrammer001,password01
id2,Poorprogrammer002,password02
id3,Poorprogrammer003,password03
id4,Poorprogrammer004,password04
id5,Poorprogrammer005,password05
id6,Poorprogrammer006,password06

ด้วย บุญเก่าทำให้เรามี applicationContext.xml แล้ว ลืมบอกไปว่า ในแต่ละ งานก็จะมีการ cofig ทั้งหมด ใน xml นะครับ อย่างที่ เจ้าพ่อ xml config อย่างสปริงเค้าถนัด แต่เรื่องของการ เขียนเป็น annotation ผมไม่แน่ใจนะครับว่าได้หรือยัง เราก็ เลยขอ config ไปที่เราไปสร้าง bean ที่จะทำการ อ่าน และคัดแยก ตัว file ของเรากัเถอะครับ จะว่าไป มานั่งเขียน คลาส เพื่ออ่าน io file ก็ดูจะถึกไปหน่อย เค้ามีของดีมาให้ใช้ครับ คือ org.springframework.batch.item.file.FlatFileItemReader เอ้อ ฟังชื่อคลาสแล้วดูดีไปดูกันเลย เราก็ไป regis มันลง applicationContext


<bean id="inputFile" class="org.springframework.core.io.ClassPathResource">
	<constructor-arg value="/users.csv" />
</bean>
<bean id="mapper"
	class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
	<property name="targetType" value="com.spring66.tutorial.model.Users" />
</bean>

<bean id="reader" class="org.springframework.batch.item.file.FlatFileItemReader">
	<property name="resource" ref="inputFile" />
	<property name="firstLineIsHeader" value="true" />
	<property name="lineTokenizer">
		<bean
			class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
			<property name="delimiter" value="," />
		</bean>
	</property>
	<property name="fieldSetMapper">
		<bean class="com.spring66.tutorial.batch.UsersMapper" />
	</property>
</bean>

เรา ก็ สร้าง reader ขึ้นมา ครับโดยมาดูกันซิว่าเรา ขาดอะไรอย่าเพิง test นะครับเด่วจะแดงแล้วก็โดนด่าเหมือนที่พี่ roofimon เค้าโดนประจำแต่ก็ TDD อ่ะนะ เอาเป็นว่าเรารู็้ทัน

<property name=”resource” ref=”inputFile”/>

ref ไป inputfie เราก็ไปขอความช่วยเหลือจาก org.springframework.core.io.ClassPathResource

<property name=”firstLineIsHeader” value=”true”/>

อันนี้หล่ะผมชอบใจ๊ชอบใจ ถึงมันจะดูเฉพาะงานไปหน่อยแต่ว่า เราก็เจอมันบ่อยเสียจริงๆ

lineTokenizer อันนี้ก็เท่ หล่อมาเลย บอกได้เลยว่าใช้ DelimitedLineTokenizer เป็นอะไรตัวนี้ น่าจะมาจาก String Tokenizer เอง

<property name=”fieldSetMapper”>
<bean class=”com.spring66.tutorial.batch.UsersMapper”/>
</property>

ตัว สุดท้ายคือตัว mapping class คุ้นๆ มั้ยครับคลาสที่ลงท้ายด้วยพวก mapper ทั้งหลาย ก็คือตัวที่จะ map model เ้ข้ากับ flele ที่อ่านมา นั้นหล่ะครับ แต่ด้วยความ ขี้ค้ราน ขั้นมหาศาลของผม เลยขอไปเอา Users model มาแก้นะครับ โดยลบ property อื่นๆ ออกให้เหลือแค่สามตัว เพื่อ map เข้ากับ csv ผม แห่ๆ

โดยเราก็ไปสร้าง คลาส Mapper ขึ้นมาหน้าตา ไม่ค่อยหล่อแบบนี้ครับ

package com.spring66.tutorial.batch;

import com.spring66.tutorial.model.Users;
import org.springframework.batch.item.file.mapping.FieldSet;
import org.springframework.batch.item.file.mapping.FieldSetMapper;

/**
 *
 * @author Phongsak
 */
public class UsersMapper implements FieldSetMapper {

	public Object mapLine(FieldSet fs) {

		Users user = new Users();

		int     idx = 0;

		user.setUsrId( fs.readString(idx++) );
		user.setUsrName( fs.readString(idx++) );
		user.setUsrPwd( fs.readString(idx++) );

		System.out.println("User is = " + user.toString());

		return user;
	}
}

สังเกตุว่า มันทำการ implements FieldSetMapper นะครับ

เพิ่ง สังเกตุอีกทีว่า โอ้ว นี่ มัน เขียนมาเยอะแ้ล้วนี่นา เลยขอ publish ก่อนนะครับ เดี๋ยวมาต่อ ภาค ต่อไป หมดแรงครับ ดองมานานแล้วด้วย อาจจะจบห้วนไปนิดนึงนะครับ แต่ เพื่อปลุกกระแสๆ อิอิ

blog แรกผมเลยนะครับผิดพลาดประการใดขออภัยครับ แนะนำผมด้วยนะครับ ขอบคุณครับ

Spring กับ Service Layer ครับ

January 27th, 2009

ขออภัยทุกท่านที่ปล่อยให้รอตอนนี้ sourcecode พร้อมแล้วที่ google code ครับ
ตอนล่าสุดนี้จะเป็นเรื่องเกี่ยวกับ Service เอ่ามีคนสงสัยว่าได้ Service เนี่ยมันมีไว้ทำมะเขืออะไรในเมื่อเราสามารถต่อตรงไปที่ DAO ได้เลย
คำตอบคือจะมีหรือไม่มีแล้วแต่ชอบ สำหรับผมมีไว้ก็ดีนะครับ
*อันดับแรก Controller เราจะไม่มีอะไรเยอะเกินไปลำพังงานใน Contoller เองก็เยอะพอดูอยู่แล้วในบางเคส ยังจะต้องไป Depend On กับ DAO อีกสี่ห้าตัว
วุ่นวายไปหน่อยไหมครับ
**อันดับที่สองเรื่อง reusable เราสามารถจัดกลุ่มยกตัวอย่างเช่น เราสามารถสร้าง service ที่ทำงานเกี่ยวกับ User ที่ถูกใช้งานบ่อยๆเก็บไว้แล้วเมื่อใดที่มีคนต้องการใช้ค่อย inject เข้าไป
ให้ Controller ซึ่งจะทำให้ Service นั้นๆถูกนำไปใช้ได้บ่อยๆและหลายๆที่
***อันดับสามเรื่อง Transaction เพราะจากการเขียน code ที่ผ่านมาเราจะเห็นว่าเราไม่ได้พูดหรือเขียน code ที่เกี่ยวข้องกับ Transaction เลยแม้แต่น้อย ดังนั้นก่อนอื่นเราต้องมาทำความเข้าใจกันก่อน
เนื่องจากบ่อยครับที่ 1 operation ต้องทำการทำงานมากว่า 1 ตารางดังนั้นเราจะทำการกำหนด scope ว่า transaction ของ operation นั้นครอบคลุมการทำงานอะไรบ้างและต้องทำให้ได้ตามมาตรฐาน
ACID เสมอ
ดังนั้นบ่อยครั้งมากที่เราจะเห็นการสร้าง Service Layer ในการสร้างแอพพลิเคชั่นด้วย Spring และใช้ Proxy Bean เข้ามาทำงานร่วมกับ Transaction Manager (เรื่องรายละเอียด เช่น Proxy Bean จะกล่าวใน
รายละเอียดในภายหลัง)
และเพื่อให้เป็นไปในแนวทาง TDD เรามาเขียน Test กันสำหรับ Service ที่เราต้องการให้มีโดยจะมี method หลักๆเท่านั้น

package com.spring66.tutorial.service;

import com.spring66.tutorial.model.Users;
import com.spring66.tutorial.model.UsersExpression;
import com.spring66.tutorial.model.UsersExpression.Criteria;
import java.util.Date;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.test.AbstractDependencyInjectionSpringContextTests;

/**
 *
 * @author TwinP
 */
public class IbatisUserServiceTest extends AbstractDependencyInjectionSpringContextTests {

    protected final Log log = LogFactory.getLog(getClass());
    private UserService usersService;

    @Override
    protected String[] getConfigLocations() {
        setAutowireMode(AUTOWIRE_BY_NAME);
        return new String[]{
                    "classpath:/applicationContext.xml",
                    "classpath*:/applicationContext.xml" // for modular projects
                };
    }

    public void testLoadBean() {
        //UserService usersService = (UserService)super.applicationContext.getBean("usersService");
        log.debug("Looking 4 UserService First");
        assertNotNull(usersService);
    }

    public void testCreateUser() {
        Users u = new Users();
        u.setLogDate(new Date());
        u.setRegDate(new Date());
        u.setUsrFirstLogin(new Date());
        u.setUsrId("1");
        u.setUsrLevel(new Integer(1));
        u.setUsrName("username");
        u.setUsrPwd("password");
        usersService.createUser(u);
        try {
            usersService.createUser(u);
            fail("How come you still can insert duplicate entry!!!!");
        } catch (Exception error) {
            log.debug("Successfully");
        }
    }

    public void testGetByExpression() {
        UsersExpression ex = new UsersExpression();
        ex.createCriteria().andUsrNameLike("user%");
        //cr.

        List<Users> users = usersService.getUsers(ex);
        log.debug("Result->" + users.size());
        assertNotNull(users);
    }

    public void testUpdateUsers() {
        UsersExpression ex = new UsersExpression();
        ex.createCriteria().andUsrNameEqualTo("username");
        List<Users> usrs = usersService.getUsers(ex);
        Users user = usrs.get(0);
        assertEquals(1, usrs.size());
        try {
            user.setUsrName(null);
            usersService.updateUsers(user);
            fail("username can not be null");
        } catch (Exception error) {
            log.debug("OK " + error.toString());
        }
    }

    public void testRemoveUsers() {
        usersService.deleteUser("username");
        UsersExpression ex = new UsersExpression();
        ex.createCriteria().andUsrNameEqualTo("username");
        List<Users> usrs = usersService.getUsers(ex);
        assertEquals(0, usrs.size());
    }

    /**
     * @param usersService the usersService to set
     */
    public void setUsersService(UserService usersService) {
        this.usersService = usersService;
    }
}

แน่นอนอย่างยิ่งว่าแดงเถือกครับดังนั้นสิ่งแรกที่เราจะต้องทำคือ สร้าง Service Interface ก่อนครับโดยมีรายละเอียดดังนี้

package com.spring66.tutorial.service;

import com.spring66.tutorial.model.Users;
import com.spring66.tutorial.model.UsersExpression;
import java.util.List;

/**
 *
 * @author TwinP
 */
public interface UserService {
    public Users createUser(Users user);
    public List<Users> getUsers(UsersExpression userEx);
    public void updateUsers(Users user);
    public void deleteUser(String userId);
}

เอ่าหายแดงแล้วครับต่อไปเรียก test ครับและสอบตกแน่นอนเพราะไม่มี bean ก้อนนี้ใน applicationContext ดังนั้นเพิ่มเข้าไปซะใน applicationContext.xml

    <bean id="usersService" class="com.spring66.tutorial.service.ibatis.IbatisUserService">
        <property name="usersDao" ref="usersDao"/>
    </bean>

หลังจากนั้นเราจะสอบตกอีกทีเพราะไม่มี implementation ครับเราต้องเขียน implementation ก่อนครับรายละเอียดดังนี้

package com.spring66.tutorial.service.ibatis;

import com.spring66.tutorial.dao.UsersDao;
import com.spring66.tutorial.model.Users;
import com.spring66.tutorial.model.UsersExpression;
import com.spring66.tutorial.service.UserService;
import java.util.List;

/**
 *
 * @author twinp
 */
public class IbatisUserService implements UserService {
    private UsersDao usersDao;
    @Override
    public Users createUser(Users user) {
        usersDao.insert(user);
        return user;
    }

    @Override
    public List<Users> getUsers(UsersExpression userEx) {
        return usersDao.selectByExpression(userEx);
    }

    @Override
    public void updateUsers(Users user) {
        usersDao.updateByPrimaryKey(user);
    }

    @Override
    public void deleteUser(String userId) {
        usersDao.deleteByPrimaryKey(userId);
    }

    /**
     * @return the usersDao
     */
    public UsersDao getUsersDao() {
        return usersDao;
    }

    /**
     * @param usersDao the usersDao to set
     */
    public void setUsersDao(UsersDao usersDao) {
        this.usersDao = usersDao;
    }

}

จากนั้น test อีกรอบครับผ่านหล่อๆเลย ตอนนี้ขอข้ามเรื่อง Transaction เก็บไว้ตอนหน้านะครับ

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 เดียวครับ
พอไหวไหมเอ่ย ==”

Spring Framework + Hibernate 3 by @somkiat

January 25th, 2009

การใช้งาน Spring Framework + Hibernate 3 ขั้นพื้นฐานสุดๆ

เห็นทางพี่ @roofimon เขียนบทความเกี่ยวกับการใช้งาน Spring + iBatis มา 2 ตอน ( เห็นว่ายังมีอีกหลายตอน ) ผมเป็นคนหนึ่งที่เคยใช้ Hibernate มาบ้าง
ก็เลยคิดว่าน่าจะมีบทความ Spring + Hibernate == GORM ( แอบขายของเล็กน้อย อิอิ ) …

โดยผมใช้ Hibernate Annotation แทน เนื่องจากผมไม่ค่อยชอบการเขียน Configuration ของ model/persistence class เท่าไรนัก และที่สำคัญผมชอบเขียน
Annotation เป้นการส่วนตัวด้วยครับ

ออกนอกเรื่องมากไปแล้วครับ เข้าเรื่องกันเลยดีกว่า

ก่อนอื่นผู้ที่เข้าใจบทความนี้ได้ดียิ่งขึ้น ควรมีพื้นฐานของ Spring Framework และ Hibernate มาบ้างครับ ….

Software and Library ที่ใช้
1. Java SE 5.0 up
2. Spring Framework 2.5 up
3. Hibernate 3 [ Core + Annotation ]
4. Apache Common DBCP 1.2.2
5. IDE => Eclipse 3.3

เพื่อให้เข้าใจง่ายๆ เนื้อหาผมจะอธิบายดังนี้ครับ
1. การออกแบบและใช้งาน Hibernate Annotation รวมถึงการทดสอบ
2. การใช้ Spring Framework ร่วมกับ Hibernate Annotation

เริ่มต้นกันเลย
1. การออกแบบและใช้งาน Hibernate Annotation รวมถึงการทดสอบ
1.1 สิ่งที่ผมจะ Demo จะเป็นเพียงการสร้าง Model/Persistence class ง่ายๆ ขึ้นมา ไม่มีอะไรซับซ้อน ประกอบไปด้วย CRUD ทั่วๆ ไป ครับ หน้าตา Table เป็นดังนี้

CREATE TABLE SPRING66_USER(
  USER_ID    NUMBER                             NOT NULL,
  USER_NAME  VARCHAR2(100 BYTE),
  PASSWORD   VARCHAR2(100 BYTE)
)

1.2 ออกแบบระบบไว้ดังนี้ครับ
- Model/Persistence class : User ทำหน้าที่นำเสนอข้อมูลจาก Table SPRING66_User
- DAO : UserDAO [ interface ] เป็น interface สำหรับกำหนด methods การทำงาน
- Implement DAO : UserDAOHibernate implements UserDAO เป็น class สำหรับ implements ส่วนการทำงานต่างๆ
- Utilities class : HibernateUtil สำหรับจัดการ Hibernate Session
- Unit Test class : UserDAOHibernate Test เป็น class สำหรับใช้ทดสอบการทำงาน
1.3 การ coding จะตาม 1.2 เลยครับ แต่ถ้าจะใช้งาน Hibernate จะต้องมี Hibernate Configuration file คือ hibernate.cfg.xml ซึ่งใช้กำหนดการติดต่อไปยัง Database
รวมทั้งกำหนด Persistence classes ที่ทำการ mapping ไปยัง Tables ต่างๆ ใน Database ครับ ซึ่งจะมีหน้าตาดังนี้

#  hibernate.cfg.xml
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
	<session-factory>
		<!-- Database connection settings -->
		<property name="connection.driver_class">oracle.jdbc.driver.OracleDriver</property>
		<property name="connection.url">jdbc:oracle:thin:@<hostname/ip>:1521:<sid>	</property>
		<property name="connection.username">username</property>
		<property name="connection.password">password</property>

		<!-- SQL dialect -->
		<property name="dialect">org.hibernate.dialect.OracleDialect</property>

		<!-- Enable Hibernate's automatic session context management -->
		<property name="current_session_context_class">thread</property>
		<!-- Disable the second-level cache -->
		<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>

		<!-- Echo all executed SQL to stdout -->
		<property name="show_sql">true</property>

		<!-- Drop and re-create the database schema on startup -->
		<property name="hbm2ddl.auto">none</property>

		<!-- Mapping -->
		<mapping class="com.spring66.model.User" />

	</session-factory>
</hibernate-configuration>

คำอธิบาย
- ส่วนการจัดการ Database Connection ซึ่งผมใช้ Oracle ดังนั้นสิ่งที่จำเป็นของ Demo นี้คือ JDBC Driver ของ Oracle
- เปลี่ยน Dialect เป็น OracleDialect
- ส่วนที่สำคัญอีกอันคือ mapping จะเป็นการอ้างถึง Model/Persistence class ที่จะใช้ จากตัวอย่างคือ User

ต่อมาทำการสร้าง Model/Persistence Class ชื่อ User.java ดังนี้

#User
@Entity
@Table( name="SPRING66_USER" )
public class User {

	@Id @GeneratedValue
	private Long user_id;
	@Column ( name="user_name" )
	private String username;
	private String password;

	/** Setter/Getter methods **/
}

คำอธิบาย
@Entity เป็นการบอกให้รู้ว่า class นี้คือ Model/Persistence class
@Table ใช้เพื่อ mapping ชื่อ table เข้ากับชื่อ class ผ่าน attribute name แต่โดยค่า default แล้วชื่อจะเหมือนกัน
@Id เป็นตัวบอกว่า property/column นั้นๆ เป็น Primary Key
@GeneratedValue จะเป็นตัวกำหนด running number ของ Primary Key โดยจะขึ้นอยู่กับชนิดของ Database ที่เราใช้ครับ จากตัวอย่างผมใช้ Oracle ดังนั้น
จำเป็นจะต้องสร้าง sequence ชื่อ hibernate_sequence มารองรับครับ
@Column จะใช้ในการ mapping ชื่อ column จะเป็นลักษณะเดียวกับ @Table

ในการออกแบบ Classes ต่างๆ นั้นผมใช้ DAO แบบง่ายดังนี้
#UserDAO เป็น interface เพื่อกำหนด methods การทำงานต่างๆ ต่อ User มีหน้าตาดังนี้

public interface UserDAO {
	public void insert( User user );
}

ดังนั้น จำเป็นจะต้องสร้าง class มา implements UserDAO ซึ่งตอนนี้เราใช้งานผ่าน Hibernate อย่างเดียว ผมจึงตั้งชื่อว่า UserDAOHibernate มีหน้าตาดังนี้

#UserDAOHibernate
public class UserDAOHibernate implements UserDAO {
	public void insert( User user ) {
		/** TO DO **/
	}
}

สิ่งที่จะขาดไปเสียไม่ได้คือ class HibernateUtil ทำหน้าที่จัดการ Hibernate Session [ ใครก็ตามที่เริ่มต้นเขียน Hibernate ต้องเจอ class หน้าตาแบบนี้แน่ๆ ]

#HibernateUtil
public class HibernateUtil {
	private static final SessionFactory sessionFactory;
	static {
		try {
			// Create the SessionFactory from hibernate.cfg.xml
			sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
		} catch (Throwable ex) {
			// Make sure you log the exception, as it might be swallowed
			System.err.println("Initial SessionFactory creation failed." + ex);
			throw new ExceptionInInitializerError(ex);
		}
	}
	public static SessionFactory getSessionFactory() {
		return sessionFactory;
	}
}

ดังนั้นมาทำการ coding UserDAOHibernate ให้เสร็จสิ้นกันไปเลย

	@Override
	public void insert(User user) {
		try {
			/** Getting the Session Factory and session */
		    	SessionFactory sf = HibernateUtil.getSessionFactory();
		    	Session session = sf.getCurrentSession();
		   	 /** Starting the Transaction */
		    	Transaction tx = session.beginTransaction();
		   	 /** Saving User */
		    	session.save(user);
		    	/** Commiting the changes */
		    	tx.commit();
	  	  	 /** Closing Session */
	   	    	sf.close();
		} catch( Exception e ) {
			e.printStackTrace();
		}
	}

คำอธิบาย
ขั้นตอนของทำงานเป็นดังนี้
1. สร้าง Hibernate Session ขึ้นมา
2. สร้างตัวจัดการ Transaction จาก Hibernate Session
3. เริ่มต้น Transaction
4. บันทึกข้อมูลของ User
5. Commit Transaction

ลองสร้าง Unit Test ขึ้นมา เพื่อทดสอบระบบงานหน่อยนึงครับ

#UserDAOHibernateTest
public class UserDAOHibernateTest extends TestCase {
	private User user = null;
	private UserDAO dao = null;

	protected void setUp() throws Exception {
		super.setUp();
		dao = new UserDAOHibernate();
	}

	protected void tearDown() throws Exception {
		super.tearDown();
		dao = null;
	}

	public void testSaveRecord() throws Exception {
		user = new User();
		user.setUsername("Somkiat");
		user.setPassword("Password");
		dao.insert(user);
		Assert.assertNotNull("primary key assigned", user.getUser_id());
	}

}

เท่านี้น่าจะพอมองเห็นภาพการใช้งาน Hibernate 3 + Annotation เพียวๆ กันไปแล้ว

แต่

มีชาวบ้านเข้าเห็นว่า ใน method inser UserDAOHibernate จะมี try/catch ไปทำโล่ห์อะไร ทำให้ code รกรุงรังไปหมด รวมทั้งจะไปจัดการ Hibernate Session,
Transaction อะไรบ่อยๆ [ ให้สังเกตว่า อะไรที่เราเขียนบ่อยๆ ซ้ำๆ จะนำไปสร้างเป็น Utilities Class หรือ Library ]

และในตอนนั้นก็มี Spring Framework ขึ้นมา เข้ามาขอรับหน้าที่ในการจัดการ Hibernate Session, Transaction …. etc. เอง
ซึ่งมั่นใจได้ว่าไม่มีหลุดแน่ๆ [ เขาขี้โม้ไว้อย่างนั้น ] ดังนั้น ไอ้เรามันคนเชื่อง่าย ก็เอามาใช้สิครับ …….

ว่าแล้วเรามาดูต่อกันไปเลยว่า Spring Framework มันมาช่วยให้โลกสงบสุขขึ้นไหม

จากแนวคิดของ Spring Framework คือ Dependency Injection และ IoC แต่มันจะยิงไปที่ไหนนั้นจะผ่านตัวจัดการคือ Bean Factory หรือโรงงานสร้างถั่วนั่นเอง
โดย Bean Factory จะสร้างสิ่งต่างๆ ได้นั้นจะต้องอ่านจาก Configuration file ดังนั้นมาดูหน้าตาของ Configuration file ว่าเป็นยังไง

#applicationContext.xml
<beans>
	<bean id="myDataSource"
		class="org.apache.commons.dbcp.BasicDataSource"
		destroy-method="close">
		<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
		<property name="url"  value="jdbc:oracle:thin:@<hostname/ip>:1521:<sid>" />
		<property name="username" value="username" />
		<property name="password" value="password" />
	</bean>

	<bean id="mySessionFactory"
		class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
		<property name="dataSource" ref="myDataSource" />
		<property name="annotatedClasses">
			<list>
				<value>com.spring66.model.User</value>
			</list>
		</property>
		<property name="hibernateProperties">
			<value>hibernate.dialect=org.hibernate.dialect.OracleDialect</value>
		</property>
	</bean>

	<bean id="UserDAO" class="com.spring66.dao.UserDAOHibernateWithSpring">
		<property name="sessionFactory" ref="mySessionFactory" />
	</bean>
</beans>

คำอธิบาย
จะมีถั่วอยู่ 3 เม็ดคือ
- myDataSource ทำหน้าที่จัดการ Database Connection โดยจะใช้งานผ่าน DBCP ซึ่งเป็นตัวจัดการ Connection Pool
- mySessionFactory ทำหน้าที่จัดการ Hibernate Session ตรงส่วนนี้เราไม่ต้องไปเขียน HibernateUtil อีกแล้ว ลดงานไปได้อีกใช่หรือเปล่า ??
- มีส่วนที่น่าสนใจคือ annotatedClasses property จะใช้สำหรับการกำหนด Model/Persistence classes ให้ Hibernate รู้จัก
- UserDAO ส่วนนี้จะถูกยิง หรือ inject มาจากผู้ใช้งาน ซึ่งกำหนดให้ยิงมาที่ class UserDAOHibernateWithSpring จะมีหน้าตาคล้ายๆ class UserDAOHibernate
แต่จะแตกต่างกันตรงที่ใช้ APIs ของ Spring Framework ซึ่งจะอธิบายต่อไป

มาดูส่วนของ class UserDAOHibernateWithSpring ซึ่งจะถูกยิง หรือ inject ผ่าน bean ชื่อ UserDAO ในการ implements นั้นผมจะใช้ HibernateDaoSupport APIs
มาช่วย ทำให้ code มันสั้นจนไม่น่าเชื่อว่าจะทำงานได้ ดังนี้

#UserDAOHibernateWithSpring
public class UserDAOHibernateWithSpring extends HibernateDaoSupport implements UserDAO {
	@Override
	public void insert(User user) {
		getHibernateTemplate().save(user);
	}
}

สั้นไหมครับ …. ไม่น่าเชื่อว่าจะทำงานได้ !!!

ต่อมาลองเขียน Unit Test ขึ้นมาอีกสักตัวครับ ชื่อ UserDAOHibernateWithSpringTest มีหน้าตาดังนี้
#UserDAOHibernateWithSpringTest

คำอธิบายขั้นตอนการทำงาน
1. ดึงข้อมูลจาก Configuration file ชื่อ applicationContext.xml
2. ทำการยิง หรือ inject ไปยัง UserDAO
dao = (UserDAO) ctx.getBean(“UserDAO”);
3. โดย DAO ที่ถูกใช้งานคือ UserDAOHibernateWithSpring ตาม Configuration file นั่นเอง
4. ทำงานบันทึกข้อมูล

เป็นอันเสร็จสิ้นการใช้งาน Spring Framework + Hibernate 3 ขั้นพื้นฐานสุดๆ ครับ

Reference Website

http://static.springframework.org/spring/docs/2.5.x/reference/orm.html