MyBatis設定の深掘り

docs

前回の記事ではもMyBatisの使い方を紹介したけど、今回はもう一歩踏み込んで、より複雑な設定や高度な機能を見ていこう。MyBatisは設定ファイル(mybatis-config.xml)とマッパーファイル(XMLまたはアノテーション)を使ってDB操作を定義するんだけど、実はこの設定ファイル、かなり奥が深いんだ。

エイリアスの活用でコードをスッキリさせる

SQLマッピングファイルでJavaの完全修飾クラス名(com.example.Userとか)を毎回書くのは面倒だよね。そんな時に便利なのがエイリアスだ。mybatis-config.xmlでタイプエイリアスを設定しておけば、短い名前でクラスを参照できるようになる。

XML

<configuration>
  <typeAliases>
    <typeAlias type="com.example.User" alias="User" />
    <package name="com.example.model"/>
  </typeAliases>
</configuration>

これで、マッパーファイルではcom.example.Userの代わりにUserと書けるようになる。パッケージ指定だと、クラス名がそのままエイリアスになるんだ。

環境ごとの設定を切り替える

開発環境、テスト環境、本番環境でDB接続情報が違う、なんてことはよくある話だよね。MyBatisでは、environmentsタグを使って複数の環境設定を定義し、実行時に切り替えることができる。

XML

<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:mem:devdb"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
      </dataSource>
    </environment>
    <environment id="production">
      <transactionManager type="MANAGED"/>
      <dataSource type="JNDI">
        <property name="initial_context" value="java:comp/env"/>
        <property name="data_source" value="jdbc/myDs"/>
      </dataSource>
    </environment>
  </environments>
</configuration>

default属性でデフォルトの環境を指定できるし、JavaコードからSqlSessionFactoryBuilderでビルドする際に環境IDを指定すれば、簡単に環境を切り替えられるよ。


マッパーファイルの活用術

基本的なSELECTやINSERTはもうバッチリかな?ここからは、もうちょっと複雑なクエリや便利な機能を見ていこう。

結果マッピングの柔軟な設定: <resultMap>

前回の記事ではシンプルなPOJOへのマッピングを紹介したけど、DBのカラム名とJavaオブジェクトのプロパティ名が一致しない場合や、複数のテーブルを結合した結果を1つのオブジェクトにマッピングしたい場合があるよね。そんな時に威力を発揮するのが**<resultMap>**だ。

例えば、usersテーブルにuser_idというカラムがあり、JavaのUserクラスにはidというプロパティがあるとしよう。

XML

<mapper namespace="com.example.UserMapper">
  <resultMap id="userResultMap" type="com.example.User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <result property="email" column="user_email"/>
  </resultMap>

  <select id="selectUserById" resultMap="userResultMap">
    SELECT user_id, user_name, user_email FROM users WHERE user_id = #{id}
  </select>
</mapper>

idタグは主キー、resultタグはそれ以外のカラムとプロパティのマッピングを指定する。これで、DBのカラム名とJavaのプロパティ名が異なっていても、MyBatisが正しくマッピングしてくれるんだ。

さらに、複雑なオブジェクトの関連も<resultMap>で表現できる。例えば、ユーザーが複数の注文を持つ場合を考えてみよう。

Java

// User.java
public class User {
    private int id;
    private String name;
    private List<Order> orders; // ユーザーは複数の注文を持つ
    // getter, setter
}

// Order.java
public class Order {
    private int orderId;
    private String productName;
    // getter, setter
}

XML

<mapper namespace="com.example.UserMapper">
  <resultMap id="userOrdersResultMap" type="com.example.User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <collection property="orders" ofType="com.example.Order"
                select="selectOrdersByUserId" column="user_id"/>
  </resultMap>

  <select id="selectUserWithOrders" resultMap="userOrdersResultMap">
    SELECT user_id, user_name FROM users WHERE user_id = #{id}
  </select>

  <select id="selectOrdersByUserId" resultType="com.example.Order">
    SELECT order_id, product_name FROM orders WHERE user_id = #{userId}
  </select>
</mapper>

ここでは、<collection>タグを使って、UserオブジェクトのordersプロパティにOrderオブジェクトのリストをマッピングしている。select属性で別のSELECT文を指定し、その結果をコレクションとして格納するんだ。これがNested Selectと呼ばれる機能だ。

SQLの再利用: <sql>タグ

複数のSQL文で同じようなWHERE句やSELECT句を使い回したい、なんて思ったことはないかな?そんな時は**<sql>タグ**を使うと便利だよ。

XML

<mapper namespace="com.example.UserMapper">
  <sql id="userColumns">
    user_id, user_name, user_email
  </sql>

  <select id="selectAllUsers" resultType="com.example.User">
    SELECT
    <include refid="userColumns"/>
    FROM users
  </select>

  <select id="selectActiveUsers" resultType="com.example.User">
    SELECT
    <include refid="userColumns"/>
    FROM users
    WHERE status = 'active'
  </select>
</mapper>

<sql>タグで定義したSQLスニペットは、<include refid="...">で参照できる。これでDRY(Don’t Repeat Yourself)原則に則ったコードが書けるようになるね。


例で理解を深める

これまでの説明だけだとイメージしにくいかもしれないから、具体的な例をいくつか見てみよう。

例1: エイリアスと環境設定

mybatis-config.xml

XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <typeAliases>
    <package name="com.example.model"/> 
  </typeAliases>

  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="org.h2.Driver"/>
        <property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
      </dataSource>
    </environment>
    <environment id="production">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="org.postgresql.Driver"/>
        <property name="url" value="jdbc:postgresql://localhost:5432/prod_db"/>
        <property name="username" value="prod_user"/>
        <property name="password" value="prod_pass"/>
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper resource="com/example/mapper/UserMapper.xml"/>
  </mappers>
</configuration>

User.java (com.example.modelパッケージ)

Java

package com.example.model;

public class User {
    private int id;
    private String name;
    private String email;

    // コンストラクタ、getter, setter
    public User() {}
    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    @Override
    public String toString() {
        return "User{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", email='" + email + '\'' +
               '}';
    }
}

UserMapper.xml (com.example.mapperパッケージ)

XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
  <insert id="insertUser" parameterType="User"> INSERT INTO users (id, name, email) VALUES (#{id}, #{name}, #{email})
  </insert>

  <select id="selectUserById" parameterType="int" resultType="User"> SELECT id, name, email FROM users WHERE id = #{id}
  </select>
</mapper>

Main.java

Java

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 com.example.model.User;
import com.example.mapper.UserMapper;

import java.io.IOException;
import java.io.Reader;
import java.sql.Connection;
import java.sql.Statement;

public class Main {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        Reader reader = Resources.getResourceAsReader(resource);

        // 開発環境用のSqlSessionFactoryをビルド
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader, "development");

        // DB初期化 (H2インメモリDB)
        try (SqlSession session = sqlSessionFactory.openSession();
             Connection conn = session.getConnection();
             Statement stmt = conn.createStatement()) {
            stmt.execute("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))");
            session.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }

        // ユーザーの挿入と取得
        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper userMapper = session.getMapper(UserMapper.class);

            User user1 = new User(1, "Alice", "alice@example.com");
            userMapper.insertUser(user1);
            session.commit();

            User fetchedUser = userMapper.selectUserById(1);
            System.out.println("取得したユーザー: " + fetchedUser);
        }
    }
}

実行結果

取得したユーザー: 
User{id=1, name='Alice', email='alice@example.com'}

例2: ResultMapとNested Select

Order.java (com.example.modelパッケージ)

Java

package com.example.model;

public class Order {
    private int orderId;
    private String productName;

    // コンストラクタ、getter, setter
    public Order() {}
    public Order(int orderId, String productName) {
        this.orderId = orderId;
        this.productName = productName;
    }

    public int getOrderId() { return orderId; }
    public void setOrderId(int orderId) { this.orderId = orderId; }
    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }

    @Override
    public String toString() {
        return "Order{" +
               "orderId=" + orderId +
               ", productName='" + productName + '\'' +
               '}';
    }
}

User.java (com.example.modelパッケージ) ※ordersリストを追加

Java

package com.example.model;

import java.util.List;

public class User {
    private int id;
    private String name;
    private String email;
    private List<Order> orders; // 新たに追加

    // コンストラクタ、getter, setter
    // ... (上記例と同じ) ...

    public List<Order> getOrders() { return orders; }
    public void setOrders(List<Order> orders) { this.orders = orders; }

    @Override
    public String toString() {
        return "User{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", email='" + email + '\'' +
               ", orders=" + orders + // ordersも表示
               '}';
    }
}

UserMapper.xml

XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
  <resultMap id="userOrdersResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <result property="email" column="user_email"/>
    <collection property="orders" ofType="Order"
                select="selectOrdersByUserId" column="user_id"/>
  </resultMap>

  <select id="selectUserWithOrders" resultMap="userOrdersResultMap">
    SELECT user_id, user_name, user_email FROM users WHERE user_id = #{id}
  </select>

  <select id="selectOrdersByUserId" resultType="Order">
    SELECT order_id, product_name FROM orders WHERE user_id = #{userId}
  </select>

  <insert id="insertUser" parameterType="User">
    INSERT INTO users (user_id, user_name, user_email) VALUES (#{id}, #{name}, #{email})
  </insert>

  <insert id="insertOrder" parameterType="Order">
    INSERT INTO orders (order_id, product_name, user_id) VALUES (#{orderId}, #{productName}, #{userId})
  </insert>
</mapper>

Main.java (DB初期化部分と呼び出し部分を変更)

Java

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 com.example.model.User;
import com.example.model.Order; // Orderをインポート
import com.example.mapper.UserMapper;

import java.io.IOException;
import java.io.Reader;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Arrays; // Arraysをインポート

public class Main {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        Reader reader = Resources.getResourceAsReader(resource);

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader, "development");

        // DB初期化 (usersとordersテーブルを作成)
        try (SqlSession session = sqlSessionFactory.openSession();
             Connection conn = session.getConnection();
             Statement stmt = conn.createStatement()) {
            stmt.execute("DROP TABLE IF EXISTS orders"); // 既存テーブル削除
            stmt.execute("DROP TABLE IF EXISTS users"); // 既存テーブル削除
            stmt.execute("CREATE TABLE users (user_id INT PRIMARY KEY, user_name VARCHAR(255), user_email VARCHAR(255))");
            stmt.execute("CREATE TABLE orders (order_id INT PRIMARY KEY, product_name VARCHAR(255), user_id INT)");
            session.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }

        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper userMapper = session.getMapper(UserMapper.class);

            // ユーザーと注文を挿入
            User userA = new User(1, "Alice", "alice@example.com");
            userMapper.insertUser(userA);

            userMapper.insertOrder(new Order(101, "Laptop", 1)); // userIdを追加
            userMapper.insertOrder(new Order(102, "Mouse", 1));
            session.commit();

            User userB = new User(2, "Bob", "bob@example.com");
            userMapper.insertUser(userB);

            userMapper.insertOrder(new Order(201, "Keyboard", 2));
            session.commit();

            // ユーザーと関連する注文を取得
            User fetchedUserWithOrders = userMapper.selectUserWithOrders(1);
            System.out.println("取得したユーザーと注文: " + fetchedUserWithOrders);
        }
    }
}

実行結果

取得したユーザーと注文: 
User
{id=1
  ,name='Alice'
  ,email='alice@example.com'
  ,orders=[Order
    {orderId=101
    ,productName='Laptop'
  },Order
    {orderId=102
    ,productName='Mouse'
  }]
}

例3: SQLの再利用 (<sql>)

例1のUserMapper.xmluserColumnsを定義し、それをselectAllUsersselectActiveUsersで再利用する。

UserMapper.xml

XML

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
  <sql id="userColumns">
    id, name, email
  </sql>

  <insert id="insertUser" parameterType="User">
    INSERT INTO users (<include refid="userColumns"/>) VALUES (#{id}, #{name}, #{email})
  </insert>

  <select id="selectAllUsers" resultType="User">
    SELECT
    <include refid="userColumns"/>
    FROM users
  </select>

  <select id="selectActiveUsers" resultType="User">
    SELECT
    <include refid="userColumns"/>
    FROM users
    WHERE email IS NOT NULL 
  </select>
</mapper>

Main.java (selectAllUsersとselectActiveUsersの呼び出しを追加)

Java

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 com.example.model.User;
import com.example.mapper.UserMapper;

import java.io.IOException;
import java.io.Reader;
import java.sql.Connection;
import java.sql.Statement;
import java.util.List;

public class Main {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        Reader reader = Resources.getResourceAsReader(resource);

        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader, "development");

        // DB初期化
        try (SqlSession session = sqlSessionFactory.openSession();
             Connection conn = session.getConnection();
             Statement stmt = conn.createStatement()) {
            stmt.execute("DROP TABLE IF EXISTS users");
            stmt.execute("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255), email VARCHAR(255))");
            session.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }

        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper userMapper = session.getMapper(UserMapper.class);

            userMapper.insertUser(new User(1, "Alice", "alice@example.com"));
            userMapper.insertUser(new User(2, "Bob", null)); // emailがnullのユーザー
            userMapper.insertUser(new User(3, "Charlie", "charlie@example.com"));
            session.commit();

            List<User> allUsers = userMapper.selectAllUsers();
            System.out.println("全てのユーザー: " + allUsers);

            List<User> activeUsers = userMapper.selectActiveUsers();
            System.out.println("アクティブなユーザー: " + activeUsers);
        }
    }
}

実行結果

全てのユーザー: 
[User
{ id=1
  ,name='Alice'
  ,email='alice@example.com'
},User
{ id=2
  ,name='Bob'
  ,email='null'
}, User
{ id=3
  ,name='Charlie'
  ,email='charlie@example.com'
}]

アクティブなユーザー: 
[User
{id=1
  ,name='Alice'
  ,email='alice@example.com'
}, User
{id=3
  ,name='Charlie'
  ,email='charlie@example.com'
}]

まとめ

今回はMyBatisの応用的な使い方として、エイリアス環境ごとの設定<resultMap>による柔軟な結果マッピングNested Select、そして**<sql>タグによるSQLの再利用**について学んだ。これらの機能を使いこなせば、より複雑なアプリケーションにもMyBatisを適用できるようになるはずだ。

MyBatisは非常に柔軟で強力なO/Rマッパーなので、ぜひ色々な機能を試してみてほしい。もし「前回の記事も見てみたい!」と思った人は、ここからチェックしてみてね!

コメント

タイトルとURLをコピーしました