MyBatisを使ってみる

提供: tknotebook
2017年2月11日 (土) 02:37時点におけるNakamuri (トーク | 投稿記録)による版

(差分) ←前の版 | 最新版 (差分) | 次の版→ (差分)
移動: 案内検索

メインページ>コンピュータの部屋#Java>MyBatis Tips


今時、まだ JDBC使っているけど、コード量が多くてうざい。 けど JPA や Hibernate はめんどくさいって思っている人いませんか?

もしそう思うなら MyBatisを試してみましょう。案外簡単、楽できます。

これは MyBatis のごく簡単なチュートリアルです。MyBatis を使ってある単独のテーブルのレコードを読む簡単な例を示します。

処理内容

この例では、MySQL flashcardデータベース上の cards テーブルの内容を読んでみます。

テーブルの形はこれです。

CREATE TABLE `cards` (
  `userid` varchar(256) NOT NULL,
  `english` varchar(256) NOT NULL,
  `japanese` varchar(256) NOT NULL,
  `english_japanese_pass_count` int(11) NOT NULL,
  `japanese_english_pass_count` int(11) NOT NULL,
  `level` int(11) NOT NULL,
  `last_review_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`english`,`userid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='フラッシュカードの単語帳';

このテーブルは、所謂単語帳で、利用者別に英単語と日本語が収録されていると考えてください。 成績や学習日時なども記録されています。

内容はこんな感じです。

練習用cardテーブルの内容.png

準備

簡単に始めるなら Maven を使うのがよいでしょう。Eclipse(現在の版は 4.6Neon)のPleiades All in one には Maven が含まれておりますので、Pleiades(Neon, All in One)とJDKをインストールすれば、開発環境は準備完了です。JDK は 今時ですから 1.8 にしてください。

データベースを読みますから、データベースも必要です。

例では MySQL を使いますが、例をそのまま動かしたいなら、ローカルマシン上に MySQL をインストールして flashcard データベースを作成し、cardsテーブルを作ってください。

プロジェクトの設定

Pleades で Mavenプロジェクトを作成したら、pom.xml の dependencies を以下のように設定します。

	<dependencies>
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.4.1</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>[5.1.40]</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.7.21</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.1.7</version>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-core</artifactId>
			<version>1.1.7</version>
		</dependency>
	</dependencies>

上の

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>[5.1.40]</version>
</dependency>

の部分は MySQL を使うために JDBC ドライバです。他のデータベースを使うなら適宜変更してください。

また、Javaコンパイラーのバージョンとソースコードのエンコーディングの指定も忘れずに。

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.target>${java.version}</maven.compiler.target>
    <maven.compiler.source>${java.version}</maven.compiler.source>
</properties>

pom.xml の設定が終わったら、Maven の「プロジェクトの更新」を実行します。


プロジェクトの src/main/resouces には、ログ出力のための logback 用の設定ファイルを置きます。 名称は logback.xml で、内容は以下のような感じでよいでしょう。

<?xml version="1.0" encoding="UTF-8" ?>

<!--suppress ALL -->
<configuration>

    <!--変数の定義を行います。2行目は外部のプロパティファイルの内容から定義します。-->
    <property name="LOG_DIR" value="c:/logback_logs" />

    <!-- 標準出力に出力する設定です。-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!--アペンダでのレベル設定-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>TRACE</level>
        </filter>
        <target>System.out</target>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n</pattern>
        </encoder>
        <withJasi>true</withJasi>
    </appender>

    <!--ログファイルを出力するシンプルな設定です。-->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <!--アペンダでのレベル設定-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>TRACE</level>
        </filter>
        <!--<file>${LOG_DIR}/Trace.log</file>-->
        <file>log/Trace.log</file>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n</pattern>
        </encoder>
    </appender>

    <!--ログファイルをアーカイブする設定です。アーカイブは1時間毎に行います。-->
    <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--アペンダでのレベル設定-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <!--<file>${LOG_DIR}/Rolling.log</file>-->
        <file>log/Rolling.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--<fileNamePattern>${LOG_DIR}/Rolling_%d{yyyy-MM-dd_HH}.log</fileNamePattern>-->
            <fileNamePattern>log/Rolling_%d{yyyy-MM-dd_HH}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{80} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- ルートから全てのログをアペンダに流し、アペンダでフィルタリングを行う方式とします。-->
    <root level="TRACE">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
        <appender-ref ref="ROLLING"/>
    </root>
</configuration>

この辺慣れている方なら、お手持ちのお気に入りの設定ファイルなどを使ってください。 log4j などを使いたい人もいるでしょう。

MyBatisの設定

ここから MyBatis の設定作りに入ります。まず、最も基本の mybatis-config.xml からです。


決まり切った設定ですが、プロジェクトの src/main/resources に mybatis-config.xml というファイルを作り 中身はこんな感じにします。

<?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>
        <!-- アンダースコアによって単語を分けている名前がCamel形式にマッチするように変更 -->
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>
    <typeAliases>
        <typeAlias alias="Card" type="info.nakamuri.app.spike.mybatisspike001.Card" />
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/flashcard" />
                <property name="username" value="MySQLのユーザ名" />
                <property name="password" value="MySQLのパスワード" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="CardMapper.xml" />
    </mappers>
</configuration>

この例では MySQLflashcard というデータベースの cards というテーブルを読みます。

info.nakamuri.app.spike.mybatisspike001.Card

というクラスは、cards テーブルのレコードの内容を受け取るクラス(DTO: Data Transfer Object)です。

後で、長々と DTO のクラス名を FQCN で記述するのはしんどいので、typeAlisa タグでは クラス名の短い名前 Card を定義しています。

environmentタグの中はおなじみの JDBC の接続パラメータです。

<transactionManager type="JDBC" />

は、トランザクションの制御をプログラムで直に制御することを表します。

<dataSource type="POOLED">

はデータベースコネクションがプールされることを示しています。

driver, url, username, password は説明するまでもないでしょう。セットアップした データベース とその中のスキーマやテーブルに合わせて 設定してください。

mappersタグは今回の主役 Mapperインターフェースを定義するファイルがどこにあるかを示しています。

以上までが、一回書けばほとんど変更しないファイルです。一度作ってひな形としておけば、 わずかな修正で使いまわせるでしょう。

Mapperの定義

さて、ここからが本題です。

Mapperとはテーブル等の読み書きに使うインターフェースです。 select, update, insert, delete 用の SQL を実行するメソッド(ステートメント)を定義します。 この例では select の例を紹介します。

mybatis-config.xml に記述した通り、mapperの定義ファイルは CardMapper.xml ですが これは CardMapper.xml をクラスパス、つまり src/main/resources に置くことを示します。

中身はこんな感じです。

<?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="info.nakamuri.app.spike.mybatisspike001.CardMapper">
    <select id="findAllCards" resultType="Card">
        select * from cards
    </select>
    <select id="findByKey" resultType="Card">
        select * from cards where userid=#{param1} and english=#{param2}
    </select>
</mapper>

定義するインターフェースは info.nakamuri.app.spike.mybatisspike001.CardMapper です。 インターフェースは2つのメソッド findAllCards と finfByKey を持ち、 findAllCards メソッドは無引数でテーブル上の全てのレコードを返します。findByKeyは 指定ユーザで指定英単語を持つレコードを1個だけ返します。

いずれも Card 型を返すように指定されていますが、実際には、戻り値は Card か Cardの コレクション型 になります。これは後で Java側のMapperインターフェースの定義で決めてやります。

SQL の中身は解説不要と思いますが、#{param1}と#{param2}は OGNLという記法で、メソッドの第1引数と第2引数を表します。 つまり、Java側のMapperインターフェースの定義では findByKey メソッドは2つの引数を取るというわけです。

Cardの定義

Cardは、Javaのデータベースプログラミングではおなじみの DTO(Data Transfer Object) です。 この例では cardsテーブル のレコードの内容そのままになっています。

Eclipseのウィザードなどを使えば、先頭に privateフィールドさえ書けば、後は自動生成できます。

もし、Javaでデータベースの仕事をする機会の多い部署なら、Excelの仕様書や、データベースクライアントの出力から 列名の一覧を読み取ってDTOのコードを吐くツールくらいはあるでしょう。実際作るのはとても容易です。

くれぐれも、項目が数百もあるレコードの DTO を全部手で書こうなんて思わないでください。 間違いの元です。

package info.nakamuri.app.spike.mybatisspike001;

import java.sql.Timestamp;

/**
 * 単語カードを表すクラス.
 */
public class Card implements Comparable<Card> {
	/**ユーザ名*/
	private String userId = null;
	/**英語*/
	private String english = null;
	/**日本語*/
	private String japanese = null;
	/**現レベルでの英語→日本語復習合格回数*/
	private int englishJapanesePassCount=0;
	/**現レベルでの日本語→英語復習合格回数*/
	private int japaneseEnglishPassCount=0;
	/**現在のレベル*/
	private int level = 1;
	/**最終復習日時*/
	private Timestamp lastReviewDate = null;

	public String getUserId() {
		return userId;
	}

	public String getEnglish() {
		return english;
	}

	public String getJapanese() {
		return japanese;
	}

	public int getEnglishJapanesePassCount() {
		return englishJapanesePassCount;
	}

	public int getJapaneseEnglishPassCount() {
		return japaneseEnglishPassCount;
	}

	public int getLevel() {
		return level;
	}

	public Timestamp getLastReviewDate() {
		return lastReviewDate;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public void setEnglish(String english) {
		this.english = english;
	}

	public void setJapanese(String japanese) {
		this.japanese = japanese;
	}

	public void setEnglishJapanesePassCount(int englishJapanesePassCount) {
		this.englishJapanesePassCount = englishJapanesePassCount;
	}

	public void setJapaneseEnglishPassCount(int japaneseEnglishPassCount) {
		this.japaneseEnglishPassCount = japaneseEnglishPassCount;
	}

	public void setLevel(int level) {
		this.level = level;
	}

	public void setLastReviewDate(Timestamp lastReviewDate) {
		this.lastReviewDate = lastReviewDate;
	}
}

セッションファクトリの作成

MyBatisでデータベースの処理を行うにはセッションが必要になります。

セッションとは JDBCの Connectionオブジェクトのようなものです。

セッションファクトリはセッションを生成するオブジェクトで、アプリケーションに、データソース毎に 一つあればよいので、シングルトンにします。

多くのアプリケーションでは定型的なコードひとつ書けばよいのですが、 複数のデータソースを使うアプリケーションでは、データソースの数だけファクトリクラスを作るのがよいでしょう。

ファクトリを作る処理はわずか2行ですが、利便性のためのシングルトン化や例外処理、セッションの生成処理などでこれだけの 大きさになってます。

package info.nakamuri.app.spike.mybatisspike001;

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;

/**
 * セッションファクトリ.
 */
public final class MyBatisSqlSessionFactory {
    /**
     * シングルトン インスタンス.
     */
    private static SqlSessionFactory sqlSessionFactory;

    /**
     * ファクトリのインスタンスの取得.
     * @return ファクトリ
     */
    public static SqlSessionFactory getSqlSessionFactory() {
        if (sqlSessionFactory == null) {
            InputStream inputStream;
            try {
                inputStream = Resources.getResourceAsStream("mybatis-config.xml");
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            } catch (IOException e) {
                throw new RuntimeException(e.getCause());
            }
        }
            return sqlSessionFactory;
    }

    /**
     * セッション開始.
     * @return セッション
     */
    public static SqlSession openSession() {
        return getSqlSessionFactory().openSession();
    }
}

Mapperインターフェースの作成

最後に Cardsテーブル用の Mapperインターフェースを作成します。基本的に CardMapper.xml で定義した通りのものを作ります。

このインターフェース定義がなくても MyBatisの昔の流儀を使えば、Mapperインターフェースは使えるのですが、 書いておいた方が後でとても楽ができます。

package info.nakamuri.app.spike.mybatisspike001;

import java.util.List;

/**
 * カードマッパーインターフェース.
 */
public interface CardMapper {
	/**
	 * 全カードの取得.
	 * @return 全カード
	 */
	List<Card> findAllCards();
	/**
	 * キーでカードを取得.
	 * @param card カード
	 * @return カード
	 */
	Card findByKey(String userid, String english);
}

findAllCards は戻り値を List で受けていることに注意してください。このようにインターフェースを定義すると MyBatisは自動的に ArrayList<Card> で 検索結果を返すように Mapperインターフェースを実装します。

Mapperインターフェースを使う

ようやくここまでたどり着きました。以下が使用例です。

Mapperインターフェースは以下のように使います。

getMapperメソッドに CardMapper.class を渡していることに注意してください。 クラスの FQCNを文字列で指定して Mapperインターフェースを呼び出す、古いやり方もあるのですが このやり方の方が間違いが入り込む余地がなく、簡単です。


このシングルトンを使って Mapperインターフェースを呼び出します。

findAllCardsの使用例

   SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();
   try {
       CardMapper cardMapper = sqlSession.getMapper(CardMapper.class);
       List<Card> cards =  cardMapper.findAllCards();
       // 何か処理
   } finally {
       sqlSession.close();
   }

findByKeyの使用例

   SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();
   try {
       CardMapper cardMapper = sqlSession.getMapper(CardMapper.class);
       Card card = cardMapper.findByKey("nakamuri", "take");
       // 何か処理
   } finally {
       sqlSession.close();
   }

これでちゃんと cardsテーブルの内容を、列を漏らさずに読むことができます。

お疲れさまでした。

補足事項

以上ですが、まだ最も肝心なところを説明していませんので補足しておきます。

JDBC で同じ処理を行うには

  1. コネクションの確立と切断
  2. select(SQL)の実行
  3. select結果をカーソルから読み取って Cardオブジェクトに変換する

の3つの仕事が必要ですが、MyBatisではどうなっているのでしょうか?

コネクションの確立はセッション生成の際、 MyBatisSqlSessionFactory が mybatis-config.xml の情報を使って行っています。 SQL の実行は Mapperインターフェースのステートメント(メソッド)の中で CardMapper.xml に記述された SQL で実行されます。

では、select結果を Cardオブジェクトへどのようにして変換しているのでしょうか?

実はこの例では、MyBatis が変換を自動的に行っているのです。JDBCでは最も手間のかかるところです。

MyBatis は レコードに対応する Cardオブジェクトを生成し、プロパティを設定します。 selectしたデータの列名と同じ名前のプロパティが DTO(Cardオブジェクト)にあれば、そのプロパティに列値がセットされます。

名前が同じかどうかの判断ではアルファベットの文字の大小は無視されます。

では englishJapanesePassCount プロパティにはなぜ english_japanese_pass_count 列の内容が 入るのでしょうか?

これは、実は mybtis-config.xml の

   <settings>
       <setting name="mapUnderscoreToCamelCase" value="true" />
   </settings>

という設定が関係しています。この設定 mapUnderscoreToCamelCase が true に設定されていた場合、 MyBatisはアンダースコアで区切られた列名をキャメル形式に変換したものとDTOのプロパティ名を比較し、マッチしたプロパティに列値をセットするのです。

以上から、Cardオブジェクトの全てのプロパティには自動的にテーブルの全ての列の値がマップされ代入されます。

いつもこううまくゆくわけではありません。しかし、列名とプロパティ名が合わないなら、SQLで列の別名を 使えばよいので問題ありません。

MyBatisでは ResultMap という機能を使えば任意の列を任意のプロパティに マップすることもできますが、これは少し手間がかかります。 これはもっと ResultMap の凝った機能を使う時に使うのがよいでしょう。ほとんどの場合は ResultMap は書かずに済みます。

まとめ

省力化に関して

たかがテーブルを1個読むのにたくさんの設定とコードが必要なのに驚いたのではないかと思います。

但し、大部分はそれぞれアプリに一個だけ必要な定型的なものばかりなので、この記事からコピペすれば すぐに作れるでしょう。プロジェクトテンプレートを用意できるIDE(IntelliJ IDEA等) なら、あらかじめ作っておいてテンプレートに登録しておきましょう。

Maven の archetype を自作したり、ひな形プロジェクトを VCS に登録しておくのもよいでしょう。これは他のライブラリでも一緒ですね。

Cardなどの JavaBean(DTO:Data Transfer Object)はEclipse等のウィザードを使えば作るのはそれほど大変ではありません。 テーブルから自動的にクラスを生成するツールも MyBatis で用意されています。そもそも DTOは MyBatisとは関係なく データーベースプログラミングでは必要なものです。

もっとも肝心な Mapperインターフェースは XML や Java の記述量が非常に少なくなるように工夫されています。

以上から、ある程度のデータベース処理の規模になれば、JDBCで直に処理を書くよりかなり省力化が期待できると思います。 基本は MapperのXML定義、MapperのJava定義、関連するDTOの定義を追加してゆくだけです。

残りの機能

MyBatis の機能は、実はこれで既に3割くらいです。かなり薄い機能のライブラリと言えます。 この他に、ResultMapの高度な機能、型変換とカスタムな型変換、更新系のSQLを実行するステートメントの定義、 動的SQLのための便利機能、アノテーション、EJBやspringとの連携などが有ります。 恐らく全てを習得するのに、JDBCでDAOを書きなれた方であれば、数日有れば十分でしょう。

JPAのようにちゃんと使おうとすると、何か月も経験を積み、思いもよらぬ振る舞いに悩まされながら 使い込んでようやく使えるようになることを考えれば、手軽に使える MyBatis は選択肢の一つとして 覚えておいて損はないと思います。

特に性能が問題になる場合は、SQLの腕さえあればなんとかなる MyBatis は心強いと思います。