MyBatisだけでも十分に素敵だけど、Guiceと組み合わせるともう最強って感じ。
せっかくなのでMyBatis+Guiceの魅力をwebappではない、スタンドアローンのプログラムを書いて堪能してみる。
mybatis: ver 3.0.6
mybatis-guice: ver 3.2
guice: ver 1.0
[1] データベースにテーブルを作る
MySQLで。
CREATE TABLE `Friends` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(45) NOT NULL, `memo` varchar(140), PRIMARY KEY (`id`), UNIQUE INDEX `name_index` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
[2] プロジェクトを作る
Mavenで。
(依存ライブラリを探してダウンロード+解凍+パス設定+などなどを、手動でやってた頃が懐かしい。)
MyBatis-GuiceはMyBatisとGuiceをつなぐ架け橋。これがあるおかげで、ものすごく便利な組み合わせになってる。
MyBatisは主要なロギングフレームワークに対応しているので、logback-classicを入れてる。
ロギングフレームワークは自動認識されるので、ライブラリをパスに追加するだけでログを吐くようになる。
<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.takumakei.study</groupId>
<artifactId>mysql-mybatis-guice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mysql-mybatis-guice</name>
<url>http://takumakei.blogspot.com/</url>
<properties>
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.0.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-guice</artifactId>
<version>3.2</version>
</dependency>
<dependency>
<groupId>com.google.code.guice</groupId>
<artifactId>guice</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.18</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>6</source>
<target>6</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
[3] レコードのモデルクラスを作る
POJOで書いてるけれど、BeanでもOK。
package com.takumakei.study.model;
public class Friend {
public int id;
public String name;
public String memo;
public Friend() { // 引数なしのコンストラクタがないとエラーになる
}
public Friend(String name, String memo) {
this.name = name;
this.memo = memo;
}
@Override
public String toString() {
return String.format("%s[%d/%s/%s]", super.toString(), id, name, memo);
}
}
[4] マッパーインターフェースを作る
このインターフェースを、MyBatis-Friends.xmlでmapperにマッピングする。
package com.takumakei.study.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.session.RowBounds;
import com.takumakei.study.model.Friend;
public interface FriendsMapper {
public void insert(Friend friend);
// DELETEしたレコードの数を返す仕様みたい
public int deleteById(int id);
// UPDATEしたレコードの数を返す仕様みたい
// 複数のパラメータを指定している。mybatis-friends.xmlでは#{0}とか#{1}で参照することになる
// こういう指定方法も可能ではあるけれど、保守性低下するから使わない方が良さそう
public int updateMemo(int id, String memo);
public Friend findById(int id);
// org.apache.ibatis.session.RowBoundsのパラメータの扱いは特殊。
// mybatis-friends.xmlではこのパラメータを明示していないけれど
// MyBatisは期待通りにコード数を制限してくれる。
public List<Friend> list(RowBounds bounds);
// Mapで取り出す例。
// 複数のパラメータを指定する際に、それぞれのパラメータに名前を付けて参照できるようにする例でもある。
// (ただし、offsetとlimitはRowBoundsを使うべきところ)
@MapKey("id")
public Map<Integer, Friend> map(@Param("offset") int offset, @Param("limit") int limit);
public int count();
}
[5] データベース操作用のクラスを作る
@Injectを指定して、friendsフィールドをGuiceに生成してもらう。
無駄に@Transactionalを使っているが、トランザクションの扱いも非常に簡単。
package com.takumakei.study.db;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.exceptions.PersistenceException;
import org.apache.ibatis.session.RowBounds;
import org.mybatis.guice.transactional.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
import com.takumakei.study.mapper.FriendsMapper;
import com.takumakei.study.model.Friend;
public class Friends {
static final Logger logger = LoggerFactory.getLogger(Friends.class);
// Guiceにインスタンスを生成してもらう
@Inject
protected FriendsMapper friends;
// 無駄に@Transactionalを指定してみた。
// org.mybatis.guiceのログレベルをDEBUGにすると、
// insertの前後でTransaction開始/終了したようなログが出力される
@Transactional
public Friend insert(String name, String memo) {
Friend friend = new Friend(name, memo);
try {
// friendのidはAUTO_INCREMENT
// insertに成功するとfriendのidを更新して返してくれる
// mybatis-friends.xmlのuseGeneratedKeysとkeyPropertyのおかげかな?
logger.info("before:{}", friend);
friends.insert(friend);
logger.info(" after:{}", friend);
return friend;
} catch (PersistenceException e) {
Throwable cause = e.getCause();
if (cause instanceof SQLIntegrityConstraintViolationException) {
// 制約に違反してinsert失敗した時にはnullを返すようにしてみた。
// ここでわざわざ例外を握りつぶす必要性は全くない。例示のための実装。
logger.warn("INSERT FAILED:{}", cause.getMessage());
return null;
}
throw e;
}
}
public boolean deleteById(int id) {
return 1 == friends.deleteById(id);
}
public boolean updateMemo(int id, String memo) {
return 1 == friends.updateMemo(id, memo);
}
public Friend findById(int id) {
return friends.findById(id);
}
public List<Friend> list(int offset, int limit) {
return friends.list(new RowBounds(offset, limit));
}
public Map<Integer, Friend> map(int offset, int limit) {
return friends.map(offset, limit);
}
public int count() {
return friends.count();
}
}
[6] SQLを書く
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.takumakei.study.mapper.FriendsMapper">
<sql id="friend">id, name, memo</sql>
<sql id="#memo">#{memo, javaType=String, jdbcType=VARCHAR}</sql>
<insert id="insert" parameterType="Friend" useGeneratedKeys="true" keyProperty="id">
INSERT INTO Friends (name, memo)
VALUES(#{name}, <include refid="#memo"/>);
</insert>
<delete id="deleteById" parameterType="int">
DELETE FROM Friends WHERE id = #{id}
</delete>
<update id="updateMemo">
UPDATE Friends
SET memo = #{1, javaType=string, jdbcType=VARCHAR}
WHERE id = #{0, javaType=int, jdbcType=INTEGER}
</update>
<select id="findById" parameterType="int" resultType="Friend">
SELECT <include refid="friend"/>
FROM Friends
WHERE id = #{id}
</select>
<select id="list" resultType="Friend">
SELECT <include refid="friend"/>
FROM Friends
ORDER BY id
</select>
<select id="map" resultType="Map">
SELECT <include refid="friend"/>
FROM Friends
ORDER BY id DESC
LIMIT #{offset}, #{limit}
</select>
<select id="count" resultType="int">
SELECT COUNT(*) FROM Friends
</select>
</mapper>
[7] mybatisの設定ファイルを作る
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties>
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="change*me" />
<property name="password" value="change*me" />
</properties>
<typeAliases>
<typeAlias alias="Friend" type="com.takumakei.study.model.Friend" />
<typeAlias alias="FriendsMapper" type="com.takumakei.study.mapper.FriendsMapper" />
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis-friends.xml" />
</mappers>
</configuration>
[8] logbackの設定ファイルを作る
設定ファイルは後で書いた方が、大量のDEBUGログを見れるので良いかもしれない。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger{136} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.apache.ibatis" level="INFO" />
<logger name="org.mybatis.guice" level="INFO" />
<logger name="java.sql.Connection" level="INFO" />
<logger name="java.sql.Statement" level="INFO" />
<logger name="java.sql.PreparedStatement" level="INFO" />
<logger name="java.sql.ResultSet" level="INFO" />
<root level="DEBUG">
<appender-ref ref="stdout"/>
</root>
</configuration>
[9] 動かす
package com.takumakei.study;
import java.sql.SQLRecoverableException;
import java.util.Date;
import java.util.Map;
import org.apache.ibatis.exceptions.PersistenceException;
import org.mybatis.guice.XMLMyBatisModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.takumakei.study.db.Friends;
import com.takumakei.study.model.Friend;
public class App {
static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
try {
logger.info("<start>");
run();
} catch (PersistenceException e) {
Throwable cause = e.getCause();
if (cause instanceof SQLRecoverableException)
logger.error("database dead? [{}] {}", cause.getClass().getSimpleName(), cause.getMessage());
} catch (Throwable t) {
logger.error("something bad", t);
} finally {
logger.info("<exit>");
}
}
public static void run() throws Exception {
Injector injector = Guice.createInjector(new XMLMyBatisModule() {
@Override
protected void initialize() {
setEnvironmentId("development");
setClassPathResource("mybatis-config.xml");
}
});
Friends db = injector.getInstance(Friends.class);
logger.info("[list up]");
for (Friend friend : db.list(0, 3)) {
logger.info("select:{}", friend);
}
logger.info("[insert]");
logger.info("insert:{}", db.insert("(1)TAKUMA KEI", "(1)hello world"));
logger.info("insert:{}", db.insert("(1)TAKUMA KEI", "(1)same name!!"));
logger.info("insert:{}", db.insert("(2)TAKUMA KEI", "(2)hello world"));
logger.info("insert:{}", db.insert("(3)TAKUMA KEI", "(3)hello world"));
logger.info("insert:{}", db.insert("(4)TAKUMA KEI", "(4)hello world"));
Friend insertedFriend = db.insert(new Date().toString(), null);
logger.info("insert:{}", insertedFriend);
logger.info("[count]");
logger.info("count:{}", db.count());
logger.info("[findById]");
logger.info("findById({}):{}", insertedFriend.id, db.findById(insertedFriend.id));
logger.info("findById(0):{}", db.findById(0));
logger.info("[update]");
logger.info("update:{}", db.updateMemo(insertedFriend.id, "HELLO HELLO HELLO"));
logger.info("update:{}", db.updateMemo(1, new Date().toString()));
logger.info("findById({}):{}", insertedFriend.id, db.findById(insertedFriend.id));
logger.info("[delete]");
logger.info("delete:{}", db.deleteById(insertedFriend.id));
logger.info("[map]");
Map<Integer, Friend> map = db.map(0, 3);
for (Integer id : map.keySet()) {
logger.info("map:{} => {}", id, map.get(id));
}
}
}
