アレについて記す

MyBatis入門

Posted on May 04, 2019 at 15:30 (JST)

MyBatis3+Java8+Mavenで試したことのメモ。

今回作成したコードはこちら

動作環境

OS: macOS Mojave ver. 10.14.2
Java: 1.8.0_181

この記事に書いてあること

  • MyBatis migrations maven plugin を使ってDBバージョン管理
  • MyBatis generator plugin を使ってDBアクセス関連コードの自動生成
  • 自動生成したコードの使用方法

MyBatis migrations maven plugin を使ってDBバージョン管理

1. migrations-maven-pluginを使用するための設定をpom.xmlにくわえる

[pom.xml(抜粋)]

<plugin>
    <groupId>org.mybatis.maven</groupId>
    <artifactId>migrations-maven-plugin</artifactId>
    <version>1.1.2</version>
    <configuration>
        <repository>migration</repository>
        <output>dist/migration-name.sql</output>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.5</version>
        </dependency>
    </dependencies>
</plugin>

2. バージョン管理に必要なファイルを自動生成する

下記コマンドを実行する。

$ ./mvnw migration:init

以下のファイルが生成される。

自動生成されたマイグレーション用ファイル

3. DB接続情報を記載する

[migration/environments/development.properties(抜粋)]

## JDBC connection properties.
driver=org.postgresql.Driver
url=jdbc:postgresql://localhost:5432/test_db
username=root
password=password

4. マイグレーション用のDDLを配備する

DDLを migration/scripts ディレクトリ直下に配備する。
ファイル名はのプリフィックス(日時)は 20xxxxxxxxxxxx_first_migration.sql より未来にすること。

例) migration/scripts/20190427130000_2nd_migration.sql

5. status確認

migration:status でマイグレーションの適用状態がわかる。

$ ./mvnw migration:status
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------< com.example:java8-maven-exercise >------------------
[INFO] Building java8-maven-exercise 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- migrations-maven-plugin:1.1.2:status (default-cli) @ java8-maven-exercise ---
[INFO] Executing  MyBatis Migration Schema StatusCommand
[INFO] ID             Applied At          Description
[INFO] ================================================================================
[INFO] 20190503172642    ...pending...    create changelog
[INFO] 20190503172643    ...pending...    first migration
[INFO] 20190427130000    ...pending...    2nd migration
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.643 s
[INFO] Finished at: 2019-05-04T02:33:26+09:00
[INFO] ------------------------------------------------------------------------

6. migration実行

migration:up を実行するとDBが最新の状態になる。

$ ./mvnw migration:up
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------< com.example:java8-maven-exercise >------------------
[INFO] Building java8-maven-exercise 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- migrations-maven-plugin:1.1.2:up (default-cli) @ java8-maven-exercise ---
[INFO] Executing  MyBatis Migration Schema UpCommand
[INFO] ========== Applying: 20190503172642_create_changelog.sql =======================
[INFO] 
[INFO] ========== Applying: 20190503172643_first_migration.sql ========================
[INFO] 
[INFO] ========== Applying: 20190427130000_2nd_migration.sql ========================
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.788 s
[INFO] Finished at: 2019-05-04T02:34:20+09:00
[INFO] ------------------------------------------------------------------------

dropをマイグレーション用Scriptに記述しておけば、migration:down でデグレーションできる。

MyBatis generator plugin を使ってDBアクセス関連コードの自動生成

1. mybatis-generator-maven-pluginを使用するための設定をpom.xmlにくわえる

[pom.xml(抜粋)]

<plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.3.7</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

2. コード生成に必要なファイルを配備する

[resources/generatorConfig.xml]

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" >
<generatorConfiguration >
    <classPathEntry location="/Users/someone/.m2/repository/org/postgresql/postgresql/42.2.5/postgresql-42.2.5.jar"/>
    <context id="context1" >
        <jdbcConnection driverClass="org.postgresql.Driver" connectionURL="jdbc:postgresql://localhost:5432/test_db" userId="root" password="password" />
        <javaModelGenerator targetPackage="com.example.exercise.generated" targetProject="src/main/java/" />
        <sqlMapGenerator targetPackage="com.example.exercise.generated" targetProject="src/main/resources/" />
        <javaClientGenerator targetPackage="com.example.exercise.generated" targetProject="src/main/java/" type="XMLMAPPER" />
        <table schema="public" tableName="%" />
    </context>
</generatorConfiguration>

targetPackage は生成したコードの出力先を指定する。
それぞれ異なるパッケージにした場合、実行時エラーとなったので全て同じパッケージにしている。

3. 自動生成コマンドを実行する

下記コマンドを実行すると指定したディレクトリにXxx.java XxxExample.java XxxMapper.java XxxMapper.xmlが生成される。

$ ./mvnw mybatis-generator:generate

Xxx.java

テーブルに対応するEntityクラス。
カラムに対応するフィールドと、それに対するGetter/Setterが定義されている。

(コード例省略。)

XxxExample.java

Where句の条件を組み立てるためのクラス。
ビルダーパターンで組み立てて使用する。

[CustomersExample.java(抜粋)]

public class CustomersExample {
    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table public.customers
     *
     * @mbg.generated Sat May 04 03:04:05 JST 2019
     */
    public Criteria createCriteria() {
        Criteria criteria = createCriteriaInternal();
        if (oredCriteria.size() == 0) {
            oredCriteria.add(criteria);
        }
        return criteria;
    }

    /**
     * This class was generated by MyBatis Generator.
     * This class corresponds to the database table public.customers
     *
     * @mbg.generated Sat May 04 03:04:05 JST 2019
     */
    protected abstract static class GeneratedCriteria {
        protected List<Criterion> criteria;

        public Criteria andFamilyNameIsNull() {
            addCriterion("family_name is null");
            return (Criteria) this;
        }

        public Criteria andFamilyNameEqualTo(String value) {
            addCriterion("family_name =", value, "familyName");
            return (Criteria) this;
        }

        public Criteria andFamilyNameNotEqualTo(String value) {
            addCriterion("family_name <>", value, "familyName");
            return (Criteria) this;
        }

        public Criteria andEmailIsNull() {
            addCriterion("email is null");
            return (Criteria) this;
        }

        public Criteria andEmailEqualTo(String value) {
            addCriterion("email =", value, "email");
            return (Criteria) this;
        }

        public Criteria andEmailLike(String value) {
            addCriterion("email like", value, "email");
            return (Criteria) this;
        }

        <他省略。すべてのフィールドに対し様々な条件が指定できる>
}

XxxMapper.java

DBアクセスにて使用頻度の高いメソッドを定義したインターフェース。
#insert#updateByPrimaryKeyなど、ここで定義されているものは自分でロジックを実装することなく利用できる。
実装は後述するXxxMapper.xmlに記載されている。

[CustomersMapper.java(抜粋)]

package com.example.exercise.generated;

import com.example.exercise.generated.Customers;
import com.example.exercise.generated.CustomersExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;

public interface CustomersMapper {
    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table public.customers
     *
     * @mbg.generated Sat May 04 03:04:05 JST 2019
     */
    List<Customers> selectByExample(CustomersExample example);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table public.customers
     *
     * @mbg.generated Sat May 04 03:04:05 JST 2019
     */
    Customers selectByPrimaryKey(Long id);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table public.customers
     *
     * @mbg.generated Sat May 04 03:04:05 JST 2019
     */
    int updateByPrimaryKey(Customers record);
}

XxxMapper.xml

DBアクセスにて使用頻度の高いメソッドを定義したinterface。
#insert#updateByPrimaryKeyなど、ここで定義されているものは自分でロジックを実装することなく利用できる。

[CustomersMapper.xml(抜粋)]

<?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.example.exercise.generated.CustomersMapper">
  <resultMap id="BaseResultMap" type="com.example.exercise.generated.Customers">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat May 04 03:04:05 JST 2019.
    -->
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="given_name" jdbcType="VARCHAR" property="givenName" />
    <result column="family_name" jdbcType="VARCHAR" property="familyName" />
    <result column="email" jdbcType="VARCHAR" property="email" />
    <result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
  </resultMap>
  <sql id="Example_Where_Clause">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat May 04 03:04:05 JST 2019.
    -->
    <where>
      <foreach collection="oredCriteria" item="criteria" separator="or">
        <if test="criteria.valid">
          <trim prefix="(" prefixOverrides="and" suffix=")">
            <foreach collection="criteria.criteria" item="criterion">
              <choose>
                <when test="criterion.noValue">
                  and ${criterion.condition}
                </when>
                <when test="criterion.singleValue">
                  and ${criterion.condition} #{criterion.value}
                </when>
                <when test="criterion.betweenValue">
                  and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                </when>
                <when test="criterion.listValue">
                  and ${criterion.condition}
                  <foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
                    #{listItem}
                  </foreach>
                </when>
              </choose>
            </foreach>
          </trim>
        </if>
      </foreach>
    </where>
  </sql>
  <sql id="Update_By_Example_Where_Clause">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat May 04 03:04:05 JST 2019.
    -->
    <where>
      <foreach collection="example.oredCriteria" item="criteria" separator="or">
        <if test="criteria.valid">
          <trim prefix="(" prefixOverrides="and" suffix=")">
            <foreach collection="criteria.criteria" item="criterion">
              <choose>
                <when test="criterion.noValue">
                  and ${criterion.condition}
                </when>
                <when test="criterion.singleValue">
                  and ${criterion.condition} #{criterion.value}
                </when>
                <when test="criterion.betweenValue">
                  and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                </when>
                <when test="criterion.listValue">
                  and ${criterion.condition}
                  <foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
                    #{listItem}
                  </foreach>
                </when>
              </choose>
            </foreach>
          </trim>
        </if>
      </foreach>
    </where>
  </sql>
  <sql id="Base_Column_List">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat May 04 03:04:05 JST 2019.
    -->
    id, given_name, family_name, email, created_at
  </sql>
  <select id="selectByExample" parameterType="com.example.exercise.generated.CustomersExample" resultMap="BaseResultMap">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat May 04 03:04:05 JST 2019.
    -->
    select
    <if test="distinct">
      distinct
    </if>
    <include refid="Base_Column_List" />
    from public.customers
    <if test="_parameter != null">
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClause != null">
      order by ${orderByClause}
    </if>
  </select>
  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat May 04 03:04:05 JST 2019.
    -->
    select 
    <include refid="Base_Column_List" />
    from public.customers
    where id = #{id,jdbcType=BIGINT}
  </select>
  <update id="updateByPrimaryKey" parameterType="com.example.exercise.generated.Customers">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat May 04 03:04:05 JST 2019.
    -->
    update public.customers
    set given_name = #{givenName,jdbcType=VARCHAR},
      family_name = #{familyName,jdbcType=VARCHAR},
      email = #{email,jdbcType=VARCHAR},
      created_at = #{createdAt,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=BIGINT}
  </update>
</mapper>

自動生成したコードの使用方法

src/main/resourcesディレクトリ下に接続情報などを記載したconfigを配備する。

[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>

    <environments default="develop">
        <environment id="develop">
            <transactionManager type="JDBC" />
            <dataSource type="POOLED">
                <property name="driver" value="org.postgresql.Driver" />
                <property name="url" value="jdbc:postgresql://localhost:5432/test_db" />
                <property name="username" value="root" />
                <property name="password" value="password" />
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.example.exercise.generated" />
    </mappers>
</configuration>

利用方法はこんな感じ。

[GeneratedCodeTest.java]

package com.example.exercise.generated;

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.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import java.io.IOException;
import java.io.Reader;
import java.sql.Date;
import java.util.List;

import static org.junit.Assert.*;

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class GeneratedCodeTest {

    private static final long PRODUCT_ID = 777;

    @BeforeClass
    public static void init() {
        try (Reader r = getConfigAsReader()) {
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(r);
            try (SqlSession session = factory.openSession()) {

                ProductsMapper mapper = session.getMapper(ProductsMapper.class);
                mapper.deleteByPrimaryKey(PRODUCT_ID);

                session.commit();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void _01_insertできる() {
        try (Reader r = getConfigAsReader()) {
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(r);
            try (SqlSession session = factory.openSession()) {

                Products product = new Products();
                product.setId(PRODUCT_ID);
                product.setName("textbook");
                product.setSalesStartDate(Date.valueOf("2019-05-01"));

                ProductsMapper mapper = session.getMapper(ProductsMapper.class);
                int countInserted = mapper.insert(product);
                assertEquals(1, countInserted);

                session.commit();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void _02_指定した項目のみupdateできる() {
        try (Reader r = getConfigAsReader()) {
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(r);
            try (SqlSession session = factory.openSession()) {

                Products product = new Products();
                product.setId(PRODUCT_ID);
                product.setSalesEndDate(Date.valueOf("2019-09-10"));

                ProductsMapper mapper = session.getMapper(ProductsMapper.class);
                int countUpdated = mapper.updateByPrimaryKeySelective(product);
                assertEquals(1, countUpdated);

                session.commit();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void _03_Exampleを使用してselectできる() {
        try (Reader r = getConfigAsReader()) {

            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(r);
            try (SqlSession session = factory.openSession()) {

                ProductsExample ex = new ProductsExample();
                ex.createCriteria()
                        .andNameLike("text%")
                        .andSalesEndDateIsNotNull();

                ProductsMapper mapper = session.getMapper(ProductsMapper.class);
                List<Products> products = mapper.selectByExample(ex);
                assertEquals(0, products.get(0).getName().indexOf("text"));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static Reader getConfigAsReader() throws IOException {
        return Resources.getResourceAsReader(Thread.currentThread().getContextClassLoader(),
                "mybatis-config.xml");
    }
}

以上。


参考URL