2011年12月1日木曜日

データベース + MyBatis + Guice のサンプル


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-GuiceMyBatisGuiceをつなぐ架け橋。これがあるおかげで、ものすごく便利な組み合わせになってる。

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));
  }
 }
}

0 件のコメント:

コメントを投稿