ブログ アーカイブ

自己紹介

本ブログをご覧いただき、ありがとうございます。
株式会社ヒューマンインタラクティブテクノロジー(HIT)の技術グループ担当者です。

弊社ホームページ

2014年4月1日火曜日

MyBatisで遊んでみる(3) 第三章 MyBatis実装サンプル

どうも、トムです。
前準備は終わりましたので第三章ではMyBatisを実装してみます。


第三章 MyBatis実装サンプル

必要なライブラリは第二章を参照してください。
本稿では第二章までの構築が完了している前提で進みます。

前回はmybatis-generatorで以下のファイルが自動生成されました。


mybatis-config.xmlの設定

まずは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>
  <settings>
    <!-- mybatisログ出力用 -->
    <setting name="logImpl" value="LOG4J"/>
  </settings>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <!-- データソース -->
      <dataSource type="POOLED">
        <property name="driver" value="org.postgresql.Driver"/>
        <property name="url" value="jdbc:postgresql://192.168.100.100:5432/test"/>
        <property name="username" value="mybatisuser"/>
        <property name="password" value="password"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <!-- Mapperファイルのパスを記述 -->
    <mapper resource="jp/hit/sample/mybatis/mapper/EmpMapper.xml"/>
    <mapper resource="jp/hit/sample/mybatis/mapper/DeptMapper.xml"/>
  </mappers>
</configuration>

今回コンソールでMyBatisが発行したSQLを確認したいのでsetting要素でLog4jでログ出力出来るように設定しています。
ロギング自体は指定しだいでLog4jの他にSLF4JやLog4j2なども使用可能です。
詳細は公式のリファレンスを参照してください。

dataSource要素ではJDBCパラメータ、mappers要素では実際のMapperのパスを記載します。



log4j.xmlの設定

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" >

  <appender name="stdout" class="org.apache.log4j.ConsoleAppender">
     <param name="Target" value="System.out" />
     <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%c{1} - %m%n" />
     </layout>
  </appender>

  <!-- SQLログを出力 -->
  <logger name="jp.hit.sample.mybatis.client" additivity="false">
    <level value="debug"/>
    <appender-ref ref="stdout"/>
  </logger>
  <!-- MyBatisSampleのデバッグログを出力 -->
  <logger name="jp.hit.sample.mybatis.logic" additivity="false">
    <level value="debug"/>
    <appender-ref ref="stdout"/>
  </logger>
  <root>
    <level value="info"/>
    <appender-ref ref="stdout"/>
  </root>
</log4j:configuration>
MyBatisが発行するSQLを確認するためにJava Client(jp.hit.sample.mybatis.client)以下のログレベルをDEBUGに設定してあります。




セッションの生成

MyBatisのセッション生成は次の実装で実現できます。
    package jp.hit.sample.mybatis.util;

import java.io.IOException;
import java.io.InputStream;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;

public class SessionUtil {
    private static Logger LOG = Logger.getLogger(SessionUtil.class);
    private static final String RESOURCE = "mybatis-config.xml";

    private SessionUtil() {
    }

    public static SqlSession createSession() {
        SqlSession session = null;
        try {
            InputStream is = Resources.getResourceAsStream(RESOURCE);
            SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(is);
            session = ssf.openSession(false);
        } catch (IOException e) {
            LOG.error(e.getMessage(), e);
        }
        return session;
    }
}
また、SqlSessionFactoryには以下6つのopenSessionメソッドが用意されており、用途に応じてセッションの生成を行う事が可能です。
SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType,TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
今回のように引数を指定せずにセッションを生成した場合はautoCommit=false(自動コミット無効)の状態でセッションが生成されます。
JDBCのトランザクション分離レベルを指定してセッションを生成したい場合は以下のように記述します。
openSession(TransactionIsolationLevel.NONE); // トランザクション無効
openSession(TransactionIsolationLevel.READ_UNCOMMITTED);  // READ UNCOMMITTED (ダーティリード有、非再現リード有、ファントムリード有)
openSession(TransactionIsolationLevel.READ_COMMITTED); // READ COMMITTED (ダーティリード無、非再現リード有、ファントムリード有)
openSession(TransactionIsolationLevel.REPEATABLE_READ); // REPEATABLE READ (ダーティリード無、非再現リード無、ファントムリード有)
openSession(TransactionIsolationLevel.SERIALIZABLE); // SERIALIZABLE (ダーティリード無、非再現リード無、ファントムリード無)
SQLステートメントの実行モードを指定したい場合は以下のように記述します。
openSession(ExecutorType.SIMPLE); // デフォルト ステートメント実行のたびにPreparedStatementを作成します。
openSession(ExecutorType.REUSE); // PreparedStatementを再利用します。
openSession(ExecutorType.BATCH); // 全ての更新ステートメントをバッチ処理します。
TransactionIsolationLevelやExecutorTypeはDBやMyBatisのバージョンにより挙動が変わるので使用する際は注意が必要です。
例えば、postgreSQLの場合はREAD_UNCOMMITTEDを指定してもダーティリードは行われません。




SQLの発行

次に生成したセッションを利用して実際にMapperファイルに記載されたSQLを発行してみます。
MyBatisを利用したSQL発行は非常にシンプルに実装できます。
    private static Logger LOG = Logger.getLogger(MyBatisSample.class);

    private static EmpMapper empMapper;
    private static DeptMapper deptMapper;

    public static void main(String[] args) {
        // セッション作成
        SqlSession session = SessionUtil.createSession();
        // Mapperを取得
        empMapper = session.getMapper(EmpMapper.class);
        deptMapper = session.getMapper(DeptMapper.class);

        try {
            // EmpMapperに自動生成されたselectByPrimaryKeyクエリを利用したPK指定でのEmpテーブルへのselect発行
            EmpKey key = new EmpKey();
            key.setId(1); // id = 1
            key.setDeptId(1); // dept_id = 1
            LOG.debug("【SQL発行】jp.hit.sample.mybatis.client.EmpMapper.selectByPrimaryKey id = 1 and dept_id = 1");
            Emp emp = empMapper.selectByPrimaryKey(key);
            printEmp(emp); // 取得したEmpデータ出力


            // EmpMapperに自動生成されたExampleMapperを使用したselect発行
            EmpExample param = new EmpExample();
            List<string> names = Arrays.asList("Bravo", "Charlie", "HIT tom");
            param.createCriteria().andNameIn(names); // IN ('Bravo','Charlie','HIT tom')
            param.setOrderByClause("dept_id desc"); // Order by dept_id desc
            LOG.debug("【SQL発行】selectByExample in ('Bravo','Charlie','HIT tom') order by dept_id desc");
            List<emp> empList = empMapper.selectByExample(param);
            printEmp(emp); // 取得したEmpデータ出力


            // Insert
            Emp insertEntity = new Emp();
            insertEntity.setId(5);
            insertEntity.setName("HIT tom");
            insertEntity.setDeptId(2);
            insertEntity.setTel("03-5225-0530");
            LOG.debug("【SQL発行】Insert Emp");
            empMapper.insert(insertEntity);


            // Insert結果取得
            empList = empMapper.selectByExample(param);
            LOG.debug("【SQL発行】selectByExample in ('Bravo','Charlie','HIT tom') order by dept_id desc");
            printEmp(emp); // 取得したEmpデータ出力

        } finally {
            // 実行前の状態にロールバックする
            session.rollback();
            // セッションを閉じる
            session.close();
        }
    }


    /**
     * Empの中身をダンプする
     * @param empList
     */
    private static void printEmp(List<emp> empList) {
        for (Emp emp : empList) {
            printEmp(emp);
        }
    }

    private static void printEmp(Emp emp) {
        if (emp != null) {
            LOG.debug("【Result】 " + "id:" + emp.getId() + ", Name:" + emp.getName() + ", Dept:"
                    + String.valueOf(emp.getDeptId()) + ", Tel:" + emp.getTel());
        }
    }

サンプル実行前のテーブルデータは以下の通り
select * from my.dept;
 id |    name
----+------------
  1 | Foundation
  2 | Sales
  3 | System

select * from my.emp;
 id | dept_id |  name   |     tel
----+---------+---------+--------------
  1 |       1 | Alfa    | 111-111-1111
  2 |       1 | Aileen  | 123-123-1234
  3 |       3 | Bravo   | 222-222-2222
  4 |       2 | Charlie | 333-333-3333

サンプルを実行すると以下の結果が得られます。
MyBatisSample - 【SQL発行】jp.hit.sample.mybatis.client.EmpMapper.selectByPrimaryKey id = 1 and dept_id = 1
selectByPrimaryKey - ooo Using Connection [org.postgresql.jdbc4.Jdbc4Connection@7bfb4d34]
selectByPrimaryKey - ==>  Preparing: select id, dept_id, name, tel from my.emp where id = ? and dept_id = ? 
selectByPrimaryKey - ==> Parameters: 1(Integer), 1(Integer)
selectByPrimaryKey - <==      Total: 1
MyBatisSample - 【Result】 id:1, Name:Alfa, Dept:1, Tel:111-111-1111
MyBatisSample - 【SQL発行】selectByExample in ('Bravo','Charlie','HIT tom') order by dept_id desc
selectByExample - ooo Using Connection [org.postgresql.jdbc4.Jdbc4Connection@7bfb4d34]
selectByExample - ==>  Preparing: select 'false' as QUERYID, id, dept_id, name, tel from my.emp WHERE ( name in ( ? , ? , ? ) ) order by dept_id desc 
selectByExample - ==> Parameters: Bravo(String), Charlie(String), HIT tom(String)
selectByExample - <==      Total: 2
MyBatisSample - 【Result】 id:3, Name:Bravo, Dept:3, Tel:222-222-2222
MyBatisSample - 【Result】 id:4, Name:Charlie, Dept:2, Tel:333-333-3333
MyBatisSample - 【SQL発行】Insert Emp
insert - ooo Using Connection [org.postgresql.jdbc4.Jdbc4Connection@7bfb4d34]
insert - ==>  Preparing: insert into my.emp (id, dept_id, name, tel) values (?, ?, ?, ?) 
insert - ==> Parameters: 5(Integer), 2(Integer), HIT tom(String), 03-5225-0530(String)
insert - <==    Updates: 1
MyBatisSample - 【SQL発行】selectByExample in ('Bravo','Charlie','HIT tom') order by dept_id desc
selectByExample - ooo Using Connection [org.postgresql.jdbc4.Jdbc4Connection@7bfb4d34]
selectByExample - ==>  Preparing: select 'false' as QUERYID, id, dept_id, name, tel from my.emp WHERE ( name in ( ? , ? , ? ) ) order by dept_id desc 
selectByExample - ==> Parameters: Bravo(String), Charlie(String), HIT tom(String)
selectByExample - <==      Total: 3
MyBatisSample - 【Result】 id:3, Name:Bravo, Dept:3, Tel:222-222-2222
MyBatisSample - 【Result】 id:4, Name:Charlie, Dept:2, Tel:333-333-3333
MyBatisSample - 【Result】 id:5, Name:HIT tom, Dept:2, Tel:03-5225-0530

サンプル実行後もロールバックが正常に働きテーブルデータが変化しない事が確認出来ます。
select * from my.dept;
 id |    name
----+------------
  1 | Foundation
  2 | Sales
  3 | System

select * from my.emp;
 id | dept_id |  name   |     tel
----+---------+---------+--------------
  1 |       1 | Alfa    | 111-111-1111
  2 |       1 | Aileen  | 123-123-1234
  3 |       3 | Bravo   | 222-222-2222
  4 |       2 | Charlie | 333-333-3333





任意のMapper作成

自動生成されたMapperの利用を確認出来たら次に新規にMapperファイルを作成して任意のSQLを発行させてみましょう。

まず、Mapperファイルと同一のパッケージ内に任意のMapper(今回はEmpDeptSqlMap.xml)を作成します。

MyBatisのマッピングは以下の基準で紐付けが行われているのですが、今回は分かりやすいように敢えてMapperの名前を「~Mapper」ではなく「SqlMap」に変えています。
  • Mapperのinterfaceクラス名 = Mapperファイルのnamespace
  • Mapperのinterfaceクラスのメソッド名 = MapperファイルのSQLID
<?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="jp.hit.sample.mybatis.client.EmpDeptSqlMap" >
  <resultMap id="EmpDeptResultMap" type="jp.hit.sample.mybatis.entity.EmpDept" >
    <id column="emp_id" property="empId" jdbcType="INTEGER" />
    <id column="dept_id" property="deptId" jdbcType="INTEGER" />
    <id column="dept_name" property="deptName" jdbcType="VARCHAR" />
    <result column="emp_name" property="empName" jdbcType="VARCHAR" />
    <result column="tel" property="tel" jdbcType="VARCHAR" />
  </resultMap>

  <!--
    resultMapを定義せずにresultTypeを指定し
    <select id="selectEmpJoinDept" resultType="jp.hit.sample.mybatis.entity.EmpDept" parameterType="jp.hit.sample.mybatis.entity.EmpDept">
    のように直接Entityクラスを戻り値に指定することも可能ですが
    内部的に余計なマッピング処理が発生する為、resultMapでjdbcTypeを定義することが推奨されています。
   -->
  <select id="selectEmpJoinDept" resultMap="EmpDeptResultMap" parameterType="jp.hit.sample.mybatis.entity.EmpDept">
    SELECT
         emp.id      AS emp_id
        ,emp.dept_id AS dept_id
        ,dept.name   AS dept_name
        ,emp.name    AS emp_name
        ,emp.tel     AS tel
    FROM my.emp emp
    LEFT JOIN my.dept dept ON emp.dept_id = dept.id

    <!--
         <where> 要素は、内包するタグのどれかが結果を返すときだけ "WHERE" を挿入します。
         更に、内包するタグから返された結果が "AND" または "OR" で始まっていた場合はこれを削除します。
         以下の例の場合 empId、deptId、empName、deptName、tel 全てがnullの場合 where要素が削除されます。
     -->
    <where>
      <if test="empId != null">
        AND emp.id = #{empId,jdbcType=INTEGER}
      </if>
      <if test="deptId != null">
        AND emp.dept_id = #{deptId,jdbcType=INTEGER}
      </if>
      <if test="empName != null">
        AND emp.name LIKE #{empName,jdbcType=VARCHAR}
      </if>
      <if test="deptName != null">
        AND dept.name LIKE #{deptName,jdbcType=VARCHAR}
      </if>
      <if test="tel != null">
        AND emp.tel = #{tel,jdbcType=VARCHAR}
      </if>
    </where>
    ORDER BY emp.id
  </select>

</mapper>
MyBatisによる動的SQLの記法については公式リファレンスを参照してください。
Mapperファイルには静的SQLをそのまま記述することも可能です。


次にmybatis-config.xmlにMapperファイルのパスを追記します。
<?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>
  <settings>
    <!-- mybatisログ出力用 -->
    <setting name="logImpl" value="LOG4J"/>
  </settings>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <!-- データソース -->
      <dataSource type="POOLED">
        <property name="driver" value="org.postgresql.Driver"/>
        <property name="url" value="jdbc:postgresql://192.168.245.131:5432/test"/>
        <property name="username" value="mybatisuser"/>
        <property name="password" value="password"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <!-- Mapperファイルのパスを記述 -->
    <mapper resource="jp/hit/sample/mybatis/mapper/EmpMapper.xml"/>
    <mapper resource="jp/hit/sample/mybatis/mapper/DeptMapper.xml"/>
    <mapper resource="jp/hit/sample/mybatis/mapper/EmpDeptSqlMap.xml"/>
  </mappers>
</configuration>

Entityを作成します。
package jp.hit.sample.mybatis.entity;

public class EmpDeptKey {
    private Integer empId;
    private Integer deptId;

    public Integer getEmpId() {
        return empId;
    }
    public void setEmpId(Integer empId) {
        this.empId = empId;
    }
    public Integer getDeptId() {
        return deptId;
    }
    public void setDeptId(Integer deptId) {
        this.deptId = deptId;
    }
}
package jp.hit.sample.mybatis.entity;

public class EmpDept extends EmpDeptKey {
    private String empName;
    private String deptName;
    private String tel;

    public String getEmpName() {
        return empName;
    }
    public void setEmpName(String empName) {
        this.empName = empName;
    }
    public String getDeptName() {
        return deptName;
    }
    public void setDeptName(String deptName) {
        this.deptName = deptName;
    }
    public String getTel() {
        return tel;
    }
    public void setTel(String tel) {
        this.tel = tel;
    }
}

Mapperのinterfaceクラスを作成します。
package jp.hit.sample.mybatis.client;

import java.util.List;

import jp.hit.sample.mybatis.entity.EmpDept;

public interface EmpDeptSqlMap {
    /**
     * SELCT * FROM Emp LEFT JOIN Dept
     */
    List<EmpDept> selectEmpJoinDept(EmpDept empDeptParam);
}



任意のMapperを利用したSQL発行

上記で作成したMapperは自動生成したMapperと同様の実装で利用できます。
    private static EmpDeptSqlMap empDeptSqlMap;

    public static void main(String[] args)  {
        // セッション作成
        SqlSession session = SessionUtil.createSession();
        // Mapperを取得
        empMapper = session.getMapper(EmpMapper.class);
        deptMapper = session.getMapper(DeptMapper.class);
        empDeptSqlMap = session.getMapper(EmpDeptSqlMap.class);

        try {
            // 手動作成したMapperを利用したSQLの発行
            EmpDept empDeptParam = new EmpDept();
            empDeptParam.setDeptName("%S%"); // dept.name Like '%S%'
            LOG.debug("【SQL発行】selectEmpJoinDept dept.name like '%S%' 】");
            List<EmpDept> empDeptList = empDeptSqlMap.selectEmpJoinDept(empDeptParam);
            printEmpDept(empDeptList);

        } finally {
            // 実行前の状態に戻す
            session.rollback();
            // セッションを閉じる
            session.close();
        }
    }

    /**
     * EmpDeptの中身をダンプする
     * @param empList
     */
    private static void printEmpDept(List<EmpDept> empDeptList) {
        for (EmpDept ed : empDeptList) {
            printEmpDept(ed);
        }
    }

    private static void printEmpDept(EmpDept empDept) {
        if (empDept != null) {
            LOG.debug("【Result】 " + "empId:" + String.valueOf(empDept.getEmpId())
                    + ", deptId:" + String.valueOf(empDept.getDeptId())
                    + ", empName:" + empDept.getEmpName()
                    + ", deptName:" + empDept.getDeptName()
                    + ", Tel:" + empDept.getTel());
        }
    }

実行すると以下の結果が得られます。
MyBatisSample - 【SQL発行】selectJoinDept dept.name like '%S%' 】
selectEmpJoinDept - ooo Using Connection [org.postgresql.jdbc4.Jdbc4Connection@2b4d13ef]
selectEmpJoinDept - ==>  Preparing: SELECT emp.id AS emp_id ,emp.dept_id AS dept_id ,dept.name AS dept_name ,emp.name AS emp_name ,emp.tel AS tel FROM my.emp emp LEFT JOIN my.dept dept ON emp.dept_id = dept.id WHERE dept.name LIKE ? ORDER BY emp.id 
selectEmpJoinDept - ==> Parameters: %S%(String)
selectEmpJoinDept - <==      Total: 2
MyBatisSample - 【Result】 empId:3, deptId:3, empName:Bravo, deptName:System, Tel:222-222-2222
MyBatisSample - 【Result】 empId:4, deptId:2, empName:Charlie, deptName:Sales, Tel:333-333-3333

Mapperファイル内には複数のSQLが記述可能なため、例えば親テーブル毎にMapperを分けるなどすれば管理もしやすくなると思います。


今回でひとまずMyBatisサンプルの動作まで確認できました
・・・が、Mapperを追加するたびに毎度設定ファイルを書き換えたり、セッション管理ロジックを書いたり、Mapperの取得したりするのはめんどくさいと思います。
MyBatisにはSpringFrameworkとシームレスに連携出来してセッションの生成、Mapperの注入などを自動で行うMyBatis-Springという拡張フレームワークが存在します。

この拡張フレームワークのおかげでMyBatisとSpringの連携は簡単に実装できるのでそちらの方がメインの使い方になると思いますが、今回はとりあえずMyBatisだけに絞りました。
MyBatis-Springの方の記事も時間があればいずれ書きたいと思います。

0 件のコメント:

コメントを投稿