Mapper XML ファイル

MyBatis の真の力は、マップされたステートメントにあります。これが魔法が起こる場所です。そのすべての力にもかかわらず、Mapper XML ファイルは比較的シンプルです。確かに、同等の JDBC コードと比較すれば、コードの 95% の節約がすぐにわかるでしょう。MyBatis は SQL に焦点を当てるように構築されており、できる限り邪魔にならないように最善を尽くします。

Mapper XML ファイルには、いくつかのファーストクラスの要素しかありません (定義する順序で)。

  • cache – 特定の名前空間のキャッシュの構成。
  • cache-ref – 別の名前空間からのキャッシュ構成への参照。
  • resultMap – データベースの結果セットからオブジェクトをロードする方法を記述する、最も複雑で強力な要素。
  • parameterMap – 非推奨! パラメータをマッピングする古い方法。インラインパラメータが推奨されており、この要素は将来削除される可能性があります。ここでは説明しません。
  • sql – 他のステートメントから参照できる再利用可能な SQL のチャンク。
  • insert – マップされた INSERT ステートメント。
  • update – マップされた UPDATE ステートメント。
  • delete – マップされた DELETE ステートメント。
  • select – マップされた SELECT ステートメント。

次のセクションでは、これらの各要素を、まずステートメント自体から詳しく説明します。

select

select ステートメントは、MyBatis で使用する最も一般的な要素の 1 つです。データベースにデータを格納することは、それを再び取り出すまではそれほど価値がないため、ほとんどのアプリケーションはデータを変更するよりもはるかに多くクエリを実行します。すべての insert、update、または delete に対して、おそらく多くの select があります。これは MyBatis の基本原則の 1 つであり、クエリと結果マッピングに多くの焦点と労力が費やされた理由です。select 要素は、単純なケースでは非常に簡単です。例:

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

このステートメントは selectPerson と呼ばれ、int (または Integer) 型のパラメータを受け取り、列名がキーで、行の値がマップされた HashMap を返します。

パラメータ表記に注目してください。

#{id}

これは、MyBatis に PreparedStatement パラメータを作成するように指示します。JDBC では、このようなパラメータは、新しい PreparedStatement に渡される SQL の "?" で識別されます。次に例を示します。

// Similar JDBC code, NOT MyBatis…
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

もちろん、結果を抽出してオブジェクトのインスタンスにマップするために、JDBC だけではさらに多くのコードが必要になります。これが MyBatis が不要にするものです。パラメータと結果のマッピングについて知っておくべきことはたくさんあります。これらの詳細は、このセクションの後半で説明します。

select 要素には、各ステートメントがどのように動作するかを詳細に構成できる属性が他にもあります。

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
select 属性
属性 説明
id このステートメントを参照するために使用できる、この名前空間内の一意の識別子。
parameterType このステートメントに渡されるパラメータの完全修飾クラス名またはエイリアス。MyBatis は、ステートメントに渡される実際のパラメータから使用する TypeHandler を計算できるため、この属性はオプションです。デフォルトは unset です。
parameterMap これは、外部の parameterMap を参照する非推奨のアプローチです。インラインパラメータマッピングと parameterType 属性を使用します。
resultType このステートメントから返される予想される型の完全修飾クラス名またはエイリアス。コレクションの場合、これはコレクション自体の型ではなく、コレクションに含まれる型である必要があります。resultType または resultMap のいずれかを使用し、両方を使用しないでください。
resultMap 外部の resultMap への名前付き参照。Result Map は MyBatis の最も強力な機能であり、それらを十分に理解すれば、多くの難しいマッピングケースを解決できます。resultMap または resultType のいずれかを使用し、両方を使用しないでください。
flushCache これを true に設定すると、このステートメントが呼び出されるたびに、ローカルキャッシュと 2 次キャッシュがフラッシュされます。デフォルト: select ステートメントの場合は false
useCache これを true に設定すると、このステートメントの結果が 2 次キャッシュにキャッシュされます。デフォルト: select ステートメントの場合は true
timeout これは、ドライバーが例外をスローする前に、データベースがリクエストから応答するのを待つ秒数を設定します。デフォルトは unset (ドライバー依存)。
fetchSize これは、ドライバーがこの設定に等しいサイズの行のバッチで結果を返すように試みるドライバーヒントです。デフォルトは unset (ドライバー依存)。
statementType STATEMENTPREPARED、または CALLABLE のいずれか。これにより、MyBatis はそれぞれ StatementPreparedStatement、または CallableStatement を使用します。デフォルト: PREPARED
resultSetType FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE|DEFAULT(unsetと同じ) のいずれか。デフォルトは unset (ドライバー依存)。
databaseId 構成された databaseIdProvider がある場合、MyBatis は databaseId 属性がないか、現在の属性と一致する databaseId を持つすべてのステートメントをロードします。同じステートメントが databaseId ありとなしで検出された場合、後者が破棄されます。
resultOrdered これは、ネストされた結果の select ステートメントにのみ適用されます。これが true の場合、新しいメインの結果行が返されたときに、以前の結果行への参照が発生しなくなるように、ネストされた結果が含まれているかグループ化されていると想定されます。これにより、ネストされた結果をはるかにメモリフレンドリーに埋めることができます。デフォルト: false
resultSets これは、複数の結果セットにのみ適用されます。ステートメントによって返される結果セットを一覧表示し、それぞれに名前を付けます。名前はカンマで区切られます。
affectData トランザクションが適切に制御されるように、データを返す INSERT、UPDATE、または DELETE ステートメントを記述する場合は、これを true に設定します。また、トランザクション制御メソッドも参照してください。デフォルト: false (3.5.12 以降)。

insert、update、delete

データ変更ステートメントの insert、update、および delete は、実装が非常に似ています。

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">
Insert、Update、および Delete 属性
属性 説明
id このステートメントを参照するために使用できる、この名前空間内の一意の識別子。
parameterType このステートメントに渡されるパラメータの完全修飾クラス名またはエイリアス。MyBatis は、ステートメントに渡される実際のパラメータから使用する TypeHandler を計算できるため、この属性はオプションです。デフォルトは unset です。
parameterMap これは、外部の parameterMap を参照する非推奨のアプローチです。インラインパラメータマッピングと parameterType 属性を使用します。
flushCache これを true に設定すると、このステートメントが呼び出されるたびに、2 次キャッシュとローカルキャッシュがフラッシュされます。デフォルト: insert、update、および delete ステートメントの場合は true
timeout これは、ドライバーが例外をスローする前に、データベースがリクエストから応答するのを待つ最大秒数を設定します。デフォルトは unset (ドライバー依存)。
statementType STATEMENTPREPARED、または CALLABLE のいずれか。これにより、MyBatis はそれぞれ StatementPreparedStatement、または CallableStatement を使用します。デフォルト: PREPARED
useGeneratedKeys (insert と update のみ) これは、MyBatis に JDBC の getGeneratedKeys メソッドを使用して、データベースによって内部的に生成されたキー (たとえば、MySQL や SQL Server のような RDBMS の自動インクリメントフィールド) を取得するように指示します。デフォルト: false
keyProperty (insert と update のみ) MyBatis が getGeneratedKeys によって返されたキー値、または insert ステートメントの selectKey 子要素によって返されたキー値を設定するプロパティを識別します。デフォルト: unset。複数の生成された列が予想される場合は、プロパティ名のカンマ区切りリストを使用できます。
keyColumn (insert と update のみ) 生成されたキーを持つテーブルの列の名前を設定します。これは、キー列がテーブルの最初の列ではない場合、特定のデータベース (PostgreSQL など) でのみ必要です。複数の生成された列が予想される場合は、列名のカンマ区切りリストを使用できます。
databaseId 構成された databaseIdProvider がある場合、MyBatis は databaseId 属性がないか、現在の属性と一致する databaseId を持つすべてのステートメントをロードします。同じステートメントが databaseId ありとなしで検出された場合、後者が破棄されます。

次に、insert、update、および delete ステートメントの例をいくつか示します。

<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

前述のように、insert は、さまざまな方法でキー生成を処理できるようにするいくつかの追加の属性とサブ要素がある点で、少し豊富です。

まず、データベースが自動生成キーフィールド (たとえば、MySQL や SQL Server) をサポートしている場合は、単に useGeneratedKeys="true" を設定し、keyProperty をターゲットプロパティに設定するだけで完了です。たとえば、上記の Author テーブルが ID に自動生成列型を使用していた場合、ステートメントは次のように変更されます。

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>

データベースが複数行の insert もサポートしている場合は、Author のリストまたは配列を渡して、自動生成されたキーを取得できます。

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username, password, email, bio) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
  </foreach>
</insert>

MyBatis には、自動生成列型をサポートしないデータベース、または自動生成キーの JDBC ドライバーのサポートをまだサポートしていないデータベースのキー生成を処理する別の方法があります。

次に、ランダムな ID を生成する簡単な (愚かな) 例を示します (これは決してしないでしょうが、柔軟性と MyBatis が本当に気にしないことを示しています)。

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

上記の例では、最初に selectKey ステートメントが実行され、Author id プロパティが設定されてから、insert ステートメントが呼び出されます。これにより、Java コードを複雑にすることなく、データベースでの自動生成キーと同様の動作が得られます。

selectKey 要素は次のように説明されています。

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
selectKey 属性
属性 説明
keyProperty selectKey ステートメントの結果を設定する必要があるターゲットプロパティ。複数の生成された列が予想される場合は、プロパティ名のカンマ区切りリストを使用できます。
keyColumn 返された結果セットで、プロパティに一致する列名。複数の生成された列が予想される場合は、列名のカンマ区切りリストを使用できます。
resultType 結果の型。MyBatis は通常これを把握できますが、念のために追加しても問題ありません。MyBatis では、文字列を含む任意の単純な型をキーとして使用できます。複数の生成された列を想定している場合は、想定されるプロパティを含む Object、または Map を使用できます。
order これは、BEFORE または AFTER に設定できます。BEFORE に設定すると、最初にキーが選択され、keyProperty が設定されてから、insert ステートメントが実行されます。AFTER に設定すると、insert ステートメントが実行され、次に selectKey ステートメントが実行されます。これは、insert ステートメント内に埋め込みシーケンス呼び出しがある可能性のある Oracle のようなデータベースで一般的です。
statementType 上記と同様に、MyBatis は StatementPreparedStatement、および CallableStatement にそれぞれマップされる STATEMENTPREPARED、および CALLABLE ステートメント型をサポートします。

不規則なケースとして、一部のデータベースでは、INSERT、UPDATE、または DELETE ステートメントで結果セットを返すことができます (たとえば、PostgreSQL および MariaDB の RETURNING 句、または MS SQL Server の OUTPUT 句)。このタイプのステートメントは、返されたデータをマップするために <select> として記述する必要があります。

<select id="insertAndGetAuthor" resultType="domain.blog.Author"
      affectData="true" flushCache="true">
  insert into Author (username, password, email, bio)
  values (#{username}, #{password}, #{email}, #{bio})
  returning id, username, password, email, bio
</select>

sql

この要素は、他のステートメントに含めることができる SQL コードの再利用可能なフラグメントを定義するために使用できます。静的に (ロードフェーズ中に) パラメータ化できます。異なるプロパティ値は、include インスタンスで異なる場合があります。例:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

このSQLフラグメントは、例えば、別のステートメントに含めることができます。

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

プロパティ値は、例えば、include句内のrefid属性またはプロパティ値でも使用できます。

<sql id="sometable">
  ${prefix}Table
</sql>

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    field1, field2, field3
  <include refid="someinclude">
    <property name="prefix" value="Some"/>
    <property name="include_target" value="sometable"/>
  </include>
</select>

パラメータ

これまでのステートメントでは、単純なパラメータの例を見てきました。パラメータはMyBatisの非常に強力な要素です。単純な状況、おそらく90%のケースでは、それほど複雑なものではありません。例えば

<select id="selectUsers" resultType="User">
  select id, username, password
  from users
  where id = #{id}
</select>

上記の例は、非常に単純な名前付きパラメータマッピングを示しています。parameterTypeがintに設定されているため、パラメータには任意の名前を付けることができます。IntegerStringなどのプリミティブまたは単純なデータ型には、関連するプロパティがないため、パラメータの完全な値がそのまま置き換えられます。ただし、複雑なオブジェクトを渡すと、動作が少し異なります。例えば

<insert id="insertUser" parameterType="User">
  insert into users (id, username, password)
  values (#{id}, #{username}, #{password})
</insert>

型がUserのパラメータオブジェクトがそのステートメントに渡された場合、id、username、passwordプロパティが検索され、それらの値がPreparedStatementパラメータに渡されます。

これは、ステートメントにパラメータを渡すのに便利で簡単です。しかし、パラメータマップには他にも多くの機能があります。

まず、MyBatisの他の部分と同様に、パラメータはより具体的なデータ型を指定できます。

#{property,javaType=int,jdbcType=NUMERIC}

MyBatisの他の部分と同様に、オブジェクトがHashMapでない限り、javaTypeはほぼ常にパラメータオブジェクトから決定できます。その場合、正しいTypeHandlerが使用されるように、javaTypeを指定する必要があります。

値としてnullが渡された場合、JDBC型はすべてのNullable列でJDBCによって必須です。PreparedStatement.setNull()メソッドのJavaDocsを読むことで、これをご自身で調べることができます。

型処理をさらにカスタマイズするために、特定のTypeHandlerクラス(またはエイリアス)を指定することもできます。例えば

#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

すでに冗長になっているように見えるかもしれませんが、実際には、これらを設定することはほとんどありません。

数値型の場合、関連する小数点以下の桁数を決定するためのnumericScaleもあります。

#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}

最後に、mode属性を使用すると、INOUT、またはINOUTパラメータを指定できます。パラメータがOUTまたはINOUTの場合、出力パラメータを呼び出す場合と同様に、パラメータオブジェクトプロパティの実際の値が変更されます。mode=OUT(またはINOUT)で、jdbcType=CURSOR(つまり、Oracle REFCURSOR)の場合、ResultSetをパラメータの型にマッピングするためにresultMapを指定する必要があります。ここで、javaType属性はオプションであることに注意してください。jdbcTypeとしてCURSORが設定されている場合、空白のままにすると、ResultSetに自動的に設定されます。

#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

MyBatisは構造体などのより高度なデータ型もサポートしていますが、出力パラメータを登録するときにステートメントに型名を伝える必要があります。例えば(繰り返しますが、実際にはこのように行を分割しないでください)

#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}

これらの強力なオプションすべてにもかかわらず、ほとんどの場合、単にプロパティ名を指定するだけで、残りはMyBatisが判断します。せいぜい、Nullable列のjdbcTypeを指定する程度でしょう。

#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}

文字列置換

デフォルトでは、#{}構文を使用すると、MyBatisはPreparedStatementプロパティを生成し、値をPreparedStatementパラメータに対して安全に設定します(例:?)。これはより安全で高速であり、ほとんどの場合推奨されますが、SQLステートメントに未変更の文字列を直接挿入したい場合もあります。例えば、ORDER BYの場合、次のようなものを使用するかもしれません

ORDER BY ${columnName}

ここでは、MyBatisは文字列を変更またはエスケープしません。

文字列置換は、SQLステートメントのメタデータ(つまり、テーブル名または列名)が動的な場合に非常に役立ちます。たとえば、任意の列でテーブルからselectする場合、次のようなコードを書く代わりに

@Select("select * from user where id = #{id}")
User findById(@Param("id") long id);

@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);

@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);

// and more "findByXxx" method

次のように記述できます

@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

ここで、${column}は直接置換され、#{value}は「準備」されます。したがって、次のように同じ作業を実行できます。

User userOfId1 = userMapper.findByColumn("id", 1L);
User userOfNameKid = userMapper.findByColumn("name", "kid");
User userOfEmail = userMapper.findByColumn("email", "noone@nowhere.com");

この考えは、テーブル名を置換する場合にも適用できます。

この方法でユーザーからの入力を受け取り、変更せずにステートメントに提供することは安全ではありません。これは、潜在的なSQLインジェクション攻撃につながるため、これらのフィールドでのユーザー入力を許可しないか、常に独自のエスケープとチェックを実行する必要があります。

Result Maps

resultMap要素は、MyBatisで最も重要で強力な要素です。これは、JDBCがResultSetからデータを取得するために必要なコードの90%を排除できるものであり、場合によってはJDBCがサポートさえしていないことを実行できます。実際、複雑なステートメントの結合マッピングのようなものと同等のコードを書くには、おそらく数千行のコードが必要になる可能性があります。ResultMapの設計は、単純なステートメントでは明示的な結果マッピングをまったく必要とせず、より複雑なステートメントでは、関係を記述するために絶対に必要なものだけを必要とするように設計されています。

明示的なresultMapを持たない単純なマップされたステートメントの例はすでに見てきました。例えば

<select id="selectUsers" resultType="map">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

このようなステートメントは、resultType属性で指定されているように、すべての列が自動的にHashMapのキーにマッピングされるという結果になります。多くの場合に役立ちますが、HashMapはドメインモデルとしてあまり適切ではありません。アプリケーションでは、ドメインモデルにJavaBeansまたはPOJO(Plain Old Java Object)を使用する可能性が高くなります。MyBatisは両方をサポートしています。次のJavaBeanを検討してください。

package com.someapp.model;
public class User {
  private int id;
  private String username;
  private String hashedPassword;

  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getHashedPassword() {
    return hashedPassword;
  }
  public void setHashedPassword(String hashedPassword) {
    this.hashedPassword = hashedPassword;
  }
}

JavaBeansの仕様に基づいて、上記のクラスにはid、username、hashedPasswordの3つのプロパティがあります。これらは、selectステートメントの列名と正確に一致します。

このようなJavaBeanは、HashMapと同じように、ResultSetに簡単にマッピングできます。

<select id="selectUsers" resultType="com.someapp.model.User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

そして、TypeAliasesはあなたの味方であることを忘れないでください。クラスの完全修飾パスを入力し続ける必要がないように、これらを使用してください。例えば

<!-- In Config XML file -->
<typeAlias type="com.someapp.model.User" alias="User"/>

<!-- In SQL Mapping XML file -->
<select id="selectUsers" resultType="User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

このような場合、MyBatisは自動的に、列を名前ベースでJavaBeanプロパティに自動マッピングするResultMapをバックグラウンドで作成しています。列名が完全に一致しない場合は、列名にselect句エイリアス(標準SQL機能)を使用してラベルを一致させることができます。例えば

<select id="selectUsers" resultType="User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from some_table
  where id = #{id}
</select>

ResultMapの素晴らしい点は、すでに多くのことを学んでいるのに、まだ一つも見ていないことです!これらの単純なケースでは、ここまでに見た以上のものは必要ありません。例として、この最後の例を外部のresultMapとして見てみましょう。これは列名の不一致を解決するもう1つの方法です。

<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>

そして、それを参照するステートメントは、resultMap属性を使用します(resultType属性を削除したことに注意してください)。例えば

<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from some_table
  where id = #{id}
</select>

世の中がいつもそれほど単純であれば良いのですが。

高度な結果マップ

MyBatisは、1つの考えに基づいて作成されました。データベースは常にあなたが望むものや必要なものであるとは限りません。すべてのデータベースが完全な第3正規形またはBCNFであるのが理想的ですが、そうではありません。また、単一のデータベースがそれを使用するすべてのアプリケーションに完全にマッピングできれば素晴らしいのですが、そうではありません。結果マップは、この問題に対してMyBatisが提供する答えです。

例えば、このステートメントをどのようにマッピングしますか?

<!-- Very Complex Statement -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
  select
       B.id as blog_id,
       B.title as blog_title,
       B.author_id as blog_author_id,
       A.id as author_id,
       A.username as author_username,
       A.password as author_password,
       A.email as author_email,
       A.bio as author_bio,
       A.favourite_section as author_favourite_section,
       P.id as post_id,
       P.blog_id as post_blog_id,
       P.author_id as post_author_id,
       P.created_on as post_created_on,
       P.section as post_section,
       P.subject as post_subject,
       P.draft as draft,
       P.body as post_body,
       C.id as comment_id,
       C.post_id as comment_post_id,
       C.name as comment_name,
       C.comment as comment_text,
       T.id as tag_id,
       T.name as tag_name
  from Blog B
       left outer join Author A on B.author_id = A.id
       left outer join Post P on B.id = P.blog_id
       left outer join Comment C on P.id = C.post_id
       left outer join Post_Tag PT on PT.post_id = P.id
       left outer join Tag T on PT.tag_id = T.id
  where B.id = #{id}
</select>

おそらく、Authorによって書かれたBlogで構成され、それぞれがゼロ以上のCommentsとTagsを持つ可能性がある複数のPostsを持つインテリジェントなオブジェクトモデルにマッピングしたいと思うでしょう。以下は、複雑なResultMapの完全な例です(Author、Blog、Post、Comments、Tagsはすべて型エイリアスであると仮定します)。見てみてください。心配しないでください。各ステップを順を追って説明します。最初は気が遠くなるように見えるかもしれませんが、実際には非常に簡単です。

<!-- Very Complex Result Map -->
<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <association property="author" javaType="Author"/>
    <collection property="comments" ofType="Comment">
      <id property="id" column="comment_id"/>
    </collection>
    <collection property="tags" ofType="Tag" >
      <id property="id" column="tag_id"/>
    </collection>
    <discriminator javaType="int" column="draft">
      <case value="1" resultType="DraftPost"/>
    </discriminator>
  </collection>
</resultMap>

resultMap要素には、いくつかのサブ要素と、議論に値する構造があります。以下は、resultMap要素の概念的なビューです。

resultMap

  • constructor - インスタンス化時にクラスのコンストラクターに結果を挿入するために使用されます
    • idArg - ID引数。結果をIDとしてフラグを立てると、全体的なパフォーマンスが向上します
    • arg - コンストラクターに挿入された通常の結果
  • id – ID結果。結果をIDとしてフラグを立てると、全体的なパフォーマンスが向上します
  • result – フィールドまたはJavaBeanプロパティに挿入された通常の結果
  • association – 複合型のアソシエーション。多くの結果がこの型にロールアップされます
    • ネストされた結果マッピング – アソシエーション自体がresultMapであるか、resultMapを参照することができます
  • collection – 複合型のコレクション
    • ネストされた結果マッピング – コレクション自体がresultMapであるか、resultMapを参照することができます
  • discriminator – 結果の値を使用して、使用するresultMapを決定します
    • case – ケースは、特定の値に基づいた結果マップです
      • ネストされた結果マッピング – ケースもそれ自体が結果マップであるため、これらの同じ要素の多くを含めることができます。または、外部のresultMapを参照することもできます。
ResultMapの属性
属性 説明
id この名前空間の一意の識別子で、この結果マップを参照するために使用できます。
type 完全修飾されたJavaクラス名、または型エイリアス(組み込み型エイリアスのリストについては、上記の表を参照してください)。
autoMapping 存在する場合、MyBatisはこのResultMapの自動マッピングを有効または無効にします。この属性は、グローバルなautoMappingBehaviorを上書きします。デフォルト:未設定。

ベストプラクティス ResultMapは常に段階的に構築してください。ここで単体テストは本当に役立ちます。上記のような巨大なresultMapを一度に構築しようとすると、間違いが発生しやすく、扱いにくくなる可能性があります。単純なものから始めて、一度に一歩ずつ進化させてください。そして、単体テストを実行してください!フレームワークを使用する欠点は、それらが時々ブラックボックス(オープンソースであるかどうかに関係なく)になることです。あなたが意図した動作を確実に達成するための最善の策は、単体テストを作成することです。バグを提出するときにも役立ちます。

次のセクションでは、各要素をより詳細に説明します。

idとresult

<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>

これらは、結果マッピングの最も基本的なものです。idresultの両方とも、単一の列値を単純なデータ型(String、int、double、Dateなど)の単一のプロパティまたはフィールドにマッピングします。

2つの違いは、id がオブジェクトインスタンスを比較する際に使用される識別子プロパティとして結果にフラグを立てる点だけです。これにより、一般的なパフォーマンス、特にキャッシュとネストされた結果マッピング(つまり、結合マッピング)のパフォーマンスを向上させるのに役立ちます。

それぞれにいくつかの属性があります。

Id と Result の属性
属性 説明
property カラムの結果をマップするフィールドまたはプロパティ。指定された名前と一致するJavaBeansプロパティが存在する場合、それが使用されます。それ以外の場合、MyBatisは指定された名前のフィールドを探します。どちらの場合も、通常のドット表記を使用して複雑なプロパティナビゲーションを使用できます。たとえば、usernameのような単純なものや、address.street.numberのようなより複雑なものにマッピングできます。
column データベースのカラム名、またはエイリアス付きのカラムラベル。これは、通常resultSet.getString(columnName)に渡されるのと同じ文字列です。
javaType 完全修飾されたJavaクラス名、またはタイプエイリアス(組み込みのタイプエイリアスのリストについては、上記の表を参照してください)。MyBatisは通常、JavaBeanにマッピングする場合、タイプを把握できます。ただし、HashMapにマッピングする場合は、目的の動作を確実にするために、javaTypeを明示的に指定する必要があります。
jdbcType このテーブルに続くサポートされているタイプのリストからのJDBCタイプ。JDBCタイプは、挿入、更新、または削除時にNULL可能なカラムに対してのみ必須です。これはMyBatisの要件ではなく、JDBCの要件です。したがって、JDBCを直接コーディングする場合でも、このタイプを指定する必要があります。ただし、NULL可能な値に対してのみです。
typeHandler 以前このドキュメントでデフォルトのタイプハンドラーについて説明しました。このプロパティを使用すると、マッピングごとにデフォルトのタイプハンドラーを上書きできます。値は、TypeHandler実装の完全修飾クラス名、またはタイプエイリアスのいずれかです。

サポートされているJDBCタイプ

今後の参考のために、MyBatisは、含まれているJdbcType列挙型を介して次のJDBCタイプをサポートしています。

BIT FLOAT CHAR TIMESTAMP OTHER UNDEFINED
TINYINT REAL VARCHAR BINARY BLOB NVARCHAR
SMALLINT DOUBLE LONGVARCHAR VARBINARY CLOB NCHAR
INTEGER NUMERIC DATE LONGVARBINARY BOOLEAN NCLOB
BIGINT DECIMAL TIME NULL CURSOR ARRAY

constructor

プロパティはほとんどのデータ転送オブジェクト(DTO)タイプのクラス、およびおそらくほとんどのドメインモデルで機能しますが、変更不可能なクラスを使用したい場合があるかもしれません。参照データやルックアップデータを含むテーブルは、ほとんどまたはまったく変更されないため、変更不可能なクラスに適しています。コンストラクタインジェクションを使用すると、パブリックメソッドを公開せずに、インスタンス化時にクラスに値を設定できます。MyBatisは、これを実現するためにプライベートプロパティとプライベートJavaBeansプロパティもサポートしていますが、コンストラクタインジェクションを好む人もいます。constructor 要素はこれを有効にします。

次のコンストラクタを検討してください。

public class User {
   //...
   public User(Integer id, String username, int age) {
     //...
  }
//...
}

結果をコンストラクタに注入するために、MyBatisは何らかの方法でコンストラクタを識別する必要があります。次の例では、MyBatisは、3つのパラメータ(java.lang.Integerjava.lang.String、およびint)で宣言されたコンストラクタをこの順序で検索します。

<constructor>
   <idArg column="id" javaType="int"/>
   <arg column="username" javaType="String"/>
   <arg column="age" javaType="_int"/>
</constructor>

多くのパラメータを持つコンストラクタを扱う場合、arg要素の順序を維持するのはエラーが発生しやすくなります。3.4.3以降、各パラメータの名前を指定することで、任意の順序でarg要素を記述できます。コンストラクタパラメータを名前で参照するには、@Paramアノテーションを追加するか、'-parameters'コンパイラオプションを使用してプロジェクトをコンパイルし、useActualParamNameを有効にします(このオプションはデフォルトで有効になっています)。次の例は、2番目と3番目のパラメータの順序が宣言された順序と一致しない場合でも、同じコンストラクタに対して有効です。

<constructor>
   <idArg column="id" javaType="int" name="id" />
   <arg column="age" javaType="_int" name="age" />
   <arg column="username" javaType="String" name="username" />
</constructor>

同じ名前とタイプの書き込み可能なプロパティがある場合は、javaTypeを省略できます。

残りの属性とルールは、通常のid要素とresult要素の場合と同じです。

属性 説明
column データベースのカラム名、またはエイリアス付きのカラムラベル。これは、通常resultSet.getString(columnName)に渡されるのと同じ文字列です。
javaType 完全修飾されたJavaクラス名、またはタイプエイリアス(組み込みのタイプエイリアスのリストについては、上記の表を参照してください)。MyBatisは通常、JavaBeanにマッピングする場合、タイプを把握できます。ただし、HashMapにマッピングする場合は、目的の動作を確実にするために、javaTypeを明示的に指定する必要があります。
jdbcType このテーブルに続くサポートされているタイプのリストからのJDBCタイプ。JDBCタイプは、挿入、更新、または削除時にNULL可能なカラムに対してのみ必須です。これはMyBatisの要件ではなく、JDBCの要件です。したがって、JDBCを直接コーディングする場合でも、このタイプを指定する必要があります。ただし、NULL可能な値に対してのみです。
typeHandler 以前このドキュメントでデフォルトのタイプハンドラーについて説明しました。このプロパティを使用すると、マッピングごとにデフォルトのタイプハンドラーを上書きできます。値は、TypeHandler実装の完全修飾クラス名、またはタイプエイリアスのいずれかです。
select このプロパティマッピングに必要な複合タイプをロードする別のマップされたステートメントのID。column属性で指定されたカラムから取得した値は、パラメータとしてターゲットのselectステートメントに渡されます。詳細については、Association要素を参照してください。
resultMap この引数のネストされた結果を適切なオブジェクトグラフにマップできるResultMapのID。これは、別のselectステートメントの呼び出しを使用する代わりに有効です。これにより、複数のテーブルを結合して1つのResultSetにすることができます。このようなResultSetには、ネストされたオブジェクトグラフに適切に分解およびマップする必要がある、重複した反復データグループが含まれます。これを容易にするために、MyBatisでは、ネストされた結果を処理するために、結果マップを「チェーン」で接続することができます。詳細については、以下のAssociation要素を参照してください。
name コンストラクタパラメータの名前。名前を指定すると、任意の順序でarg要素を記述できます。上記の説明を参照してください。3.4.3以降。

association

<association property="author" javaType="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
</association>

association要素は、「has-one」タイプの関係を処理します。たとえば、この例では、ブログには1人の作成者がいます。アソシエーションマッピングは、他の結果とほとんど同じように機能します。ターゲットのプロパティ、プロパティのjavaType(MyBatisがほとんどの場合把握できる)、必要な場合はjdbcType、および結果値の取得を上書きする場合はtypeHandlerを指定します。

アソシエーションが異なるのは、アソシエーションのロード方法をMyBatisに指示する必要があることです。MyBatisは2つの異なる方法でこれを行うことができます。

  • ネストされたSelect:目的の複雑なタイプを返す別のマップされたSQLステートメントを実行することにより。
  • ネストされた結果:結合された結果の反復サブセットを処理するために、ネストされた結果マッピングを使用することにより。

まず、要素のプロパティを調べましょう。ご覧のとおり、select属性とresultMap属性のみが通常の結果マッピングとは異なります。

属性 説明
property カラムの結果をマップするフィールドまたはプロパティ。指定された名前と一致するJavaBeansプロパティが存在する場合、それが使用されます。それ以外の場合、MyBatisは指定された名前のフィールドを探します。どちらの場合も、通常のドット表記を使用して複雑なプロパティナビゲーションを使用できます。たとえば、usernameのような単純なものや、address.street.numberのようなより複雑なものにマッピングできます。
javaType 完全修飾されたJavaクラス名、またはタイプエイリアス(組み込みのタイプエイリアスのリストについては、上記の表を参照してください)。MyBatisは通常、JavaBeanにマッピングする場合、タイプを把握できます。ただし、HashMapにマッピングする場合は、目的の動作を確実にするために、javaTypeを明示的に指定する必要があります。
jdbcType このテーブルに続くサポートされているタイプのリストからのJDBCタイプ。JDBCタイプは、挿入、更新、または削除時にNULL可能なカラムに対してのみ必須です。これはMyBatisの要件ではなく、JDBCの要件です。したがって、JDBCを直接コーディングする場合でも、このタイプを指定する必要があります。ただし、NULL可能な値に対してのみです。
typeHandler 以前このドキュメントでデフォルトのタイプハンドラーについて説明しました。このプロパティを使用すると、マッピングごとにデフォルトのタイプハンドラーを上書きできます。値は、TypeHandler実装の完全修飾クラス名、またはタイプエイリアスのいずれかです。

アソシエーションのネストされたSelect

属性 説明
column データベースのカラム名、またはネストされたステートメントに入力パラメータとして渡される値を保持するエイリアス付きカラムラベル。これは、通常resultSet.getString(columnName)に渡されるのと同じ文字列です。注:複合キーを処理するには、構文column="{prop1=col1,prop2=col2}"を使用して、ネストされたselectステートメントに渡す複数のカラム名を指定できます。これにより、ターゲットのネストされたselectステートメントのパラメータオブジェクトに対してprop1prop2が設定されます。
select このプロパティマッピングに必要な複合タイプをロードする別のマップされたステートメントのID。column属性で指定されたカラムから取得した値は、パラメータとしてターゲットのselectステートメントに渡されます。詳細な例がこの表の後に続きます。注:複合キーを処理するには、構文column="{prop1=col1,prop2=col2}"を使用して、ネストされたselectステートメントに渡す複数のカラム名を指定できます。これにより、ターゲットのネストされたselectステートメントのパラメータオブジェクトに対してprop1prop2が設定されます。
fetchType オプション。有効な値はlazyeagerです。存在する場合、このマッピングのグローバル構成パラメータlazyLoadingEnabledを置き換えます。

たとえば

<resultMap id="blogResult" type="Blog">
  <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectAuthor" resultType="Author">
  SELECT * FROM AUTHOR WHERE ID = #{id}
</select>

以上です。ブログをロードするselectステートメントと、作成者をロードする別のステートメントの2つのselectステートメントがあり、ブログのresultMapは、selectAuthorステートメントを使用して作成者プロパティをロードする必要があることを記述しています。

その他のすべてのプロパティは、カラム名とプロパティ名が一致することを前提として自動的にロードされます。

このアプローチは単純ですが、大規模なデータセットやリストではうまく機能しません。この問題は「N+1 Selects Problem」として知られています。簡単に言えば、N+1 select問題は次のように発生します。

  • 単一のSQLステートメントを実行してレコードのリストを取得します(「+1」)。
  • 返された各レコードに対して、各詳細をロードするselectステートメントを実行します(「N」)。

この問題により、数百または数千のSQLステートメントが実行される可能性があります。これは必ずしも望ましいことではありません。

良い点は、MyBatisがこのようなクエリを遅延ロードできるため、これらのステートメントのコストを一度に免れる可能性があることです。ただし、このようなリストをロードして、すぐに反復処理してネストされたデータにアクセスすると、すべての遅延ロードが呼び出されるため、パフォーマンスが非常に悪くなる可能性があります。

そのため、別の方法があります。

アソシエーションのネストされた結果

属性 説明
resultMap このアソシエーションのネストされた結果を適切なオブジェクトグラフにマップできるResultMapのID。これは、別のselectステートメントの呼び出しを使用する代わりに有効です。これにより、複数のテーブルを結合して1つのResultSetにすることができます。このようなResultSetには、ネストされたオブジェクトグラフに適切に分解およびマップする必要がある、重複した反復データグループが含まれます。これを容易にするために、MyBatisでは、ネストされた結果を処理するために、結果マップを「チェーン」で接続することができます。例ははるかに理解しやすく、この表の後に続きます。
columnPrefix 複数のテーブルを結合する場合、ResultSetでカラム名の重複を避けるために、カラムエイリアスを使用する必要があります。columnPrefixを指定すると、このようなカラムを外部のresultMapにマップできます。このセクションの後半で説明する例を参照してください。
notNullColumn デフォルトでは、子オブジェクトは、子オブジェクトのプロパティにマッピングされたカラムの少なくとも1つがNULLでない場合にのみ作成されます。この属性を使用すると、MyBatisが子オブジェクトを作成するのが、どのカラムに値が必要かを指定することでこの動作を変更できます。複数のカラム名は、コンマを区切り文字として使用して指定できます。デフォルト値:設定解除。
autoMapping 存在する場合、MyBatisは、このプロパティに結果をマッピングするときに自動マッピングを有効または無効にします。この属性は、グローバルなautoMappingBehaviorを上書きします。外部のresultMapには影響しないため、select属性またはresultMap属性と一緒に使用しても意味がないことに注意してください。デフォルト値:設定解除。

上記のネストされたアソシエーションの非常に複雑な例はすでに見てきました。以下は、これがどのように機能するかを示すための、はるかに単純な例です。別のステートメントを実行する代わりに、次のようにブログテーブルと作成者テーブルを結合します。

<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    B.author_id     as blog_author_id,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>

結合と、すべての結果が一意で明確な名前でエイリアスされていることを確認するために注意が払われていることに注目してください。これにより、マッピングがはるかに簡単になります。これで結果をマッピングできます。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" resultMap="authorResult" />
</resultMap>

<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>

上記の例では、Blogの「author」関連付けが、「authorResult」resultMapに委譲してAuthorインスタンスをロードしていることがわかります。

非常に重要 id要素は、ネストされた結果マッピングにおいて非常に重要な役割を果たします。結果を一意に識別するために使用できる1つ以上のプロパティを常に指定する必要があります。実際には、省略してもMyBatisは動作しますが、パフォーマンスが大幅に低下します。結果を一意に識別できるプロパティをできるだけ少なく選択してください。プライマリキーは明白な選択肢です(複合キーの場合でも)。

さて、上記の例では、関連付けをマップするために外部のresultMap要素を使用しました。これにより、AuthorのresultMapを再利用できます。ただし、再利用する必要がない場合、または単に結果マッピングを1つの記述的なresultMapにまとめて配置したい場合は、関連付けの結果マッピングをネストできます。このアプローチを使用した同じ例を次に示します。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
  </association>
</resultMap>

ブログに共同著者がいる場合はどうでしょうか?select文は次のようになります。

<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio,
    CA.id           as co_author_id,
    CA.username     as co_author_username,
    CA.password     as co_author_password,
    CA.email        as co_author_email,
    CA.bio          as co_author_bio
  from Blog B
  left outer join Author A on B.author_id = A.id
  left outer join Author CA on B.co_author_id = CA.id
  where B.id = #{id}
</select>

AuthorのresultMapは次のように定義されていることを思い出してください。

<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>

結果の列名がresultMapで定義されている列と異なるため、共同著者の結果をマッピングするためにresultMapを再利用するには、columnPrefixを指定する必要があります。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author"
    resultMap="authorResult" />
  <association property="coAuthor"
    resultMap="authorResult"
    columnPrefix="co_" />
</resultMap>

関連付けのための複数の結果セット

属性 説明
column 複数の結果セットを使用する場合、この属性は、リレーションシップの親と子を識別するためにforeignColumnと関連付けられる列を(カンマで区切って)指定します。
foreignColumn 親タイプのcolumn属性で指定された列の値と照合される外部キーを含む列の名前を識別します。
resultSet この複合型がロードされる結果セットの名前を識別します。

MyBatisバージョン3.2.3以降では、N+1問題を解決する別の方法が提供されています。

一部のデータベースでは、ストアドプロシージャが複数の結果セットを返すことや、複数のステートメントを一度に実行して、それぞれに対して結果セットを返すことができます。これを利用して、結合を使用せずに一度だけデータベースにアクセスし、関連データを返すことができます。

例では、ストアドプロシージャは次のクエリを実行し、2つの結果セットを返します。最初はBlogsが含まれ、2番目はAuthorsが含まれます。

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM AUTHOR WHERE ID = #{id}

カンマで区切られた名前のリストを持つresultSets属性をマップされたステートメントに追加することにより、各結果セットに名前を付ける必要があります。

<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
  {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>

これで、「author」関連付けを埋めるデータが「authors」結果セットにあることを指定できます。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <result property="email" column="email"/>
    <result property="bio" column="bio"/>
  </association>
</resultMap>

上記では、「has one」型の関連付けを処理する方法を見てきました。では、「has many」の場合はどうでしょうか?それが次のセクションの主題です。

collection

<collection property="posts" ofType="domain.blog.Post">
  <id property="id" column="post_id"/>
  <result property="subject" column="post_subject"/>
  <result property="body" column="post_body"/>
</collection>

collection要素は、関連付けとほぼ同じように機能します。実際、非常によく似ているため、類似点をドキュメント化するのは冗長になります。したがって、違いに焦点を当てましょう。

上記の例を続けると、BlogにはAuthorが1人しかいませんでした。しかし、Blogには多くのPostがあります。ブログクラスでは、これは次のようになります。

private List<Post> posts;

このようにネストされた結果のセットをListにマッピングするには、collection要素を使用します。関連付け要素と同様に、ネストされたselect、または結合からのネストされた結果を使用できます。

コレクションのネストされたSelect

まず、ネストされたselectを使用してBlogのPostをロードすることを見てみましょう。

<resultMap id="blogResult" type="Blog">
  <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectPostsForBlog" resultType="Post">
  SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

すぐに気づくことがたくさんありますが、ほとんどの場合、上記で学習した関連付け要素と非常によく似ています。まず、collection要素を使用していることに気付くでしょう。次に、新しい「ofType」属性があることに気付くでしょう。この属性は、JavaBean(またはフィールド)プロパティの型と、コレクションに含まれる型を区別するために必要です。したがって、次のマッピングをこのように読むことができます。

<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>

「Post型のArrayList内の投稿のコレクション」と読みます。

javaType属性は、ほとんどの場合MyBatisが自動的に判断するため、実際には不要です。したがって、多くの場合、次のように短縮できます。

<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>

コレクションのネストされた結果

この時点で、コレクションのネストされた結果がどのように機能するかを推測できるでしょう。関連付けとまったく同じですが、ofType属性が追加されています。

まず、SQLを見てみましょう。

<select id="selectBlog" resultMap="blogResult">
  select
  B.id as blog_id,
  B.title as blog_title,
  B.author_id as blog_author_id,
  P.id as post_id,
  P.subject as post_subject,
  P.body as post_body,
  from Blog B
  left outer join Post P on B.id = P.blog_id
  where B.id = #{id}
</select>

繰り返しますが、BlogとPostテーブルを結合し、単純なマッピングのために高品質の結果列ラベルを確保するように注意しました。これで、Postマッピングのコレクションを持つBlogのマッピングは、次のように簡単になりました。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <result property="body" column="post_body"/>
  </collection>
</resultMap>

繰り返しますが、ここでのid要素の重要性を覚えておいてください。または、まだ読んでいない場合は、上記の関連付けセクションをお読みください。

また、resultMapの再利用性を高める長い形式の方が好ましい場合は、次の代替マッピングを使用できます。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>

<resultMap id="blogPostResult" type="Post">
  <id property="id" column="id"/>
  <result property="subject" column="subject"/>
  <result property="body" column="body"/>
</resultMap>

コレクションの複数の結果セット

関連付けで行ったように、2つのクエリを実行して2つの結果セット(1つはBlogs、もう1つはPosts)を返すストアドプロシージャを呼び出すことができます。

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM POST WHERE BLOG_ID = #{id}

カンマで区切られた名前のリストを持つresultSets属性をマップされたステートメントに追加することにより、各結果セットに名前を付ける必要があります。

<select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
  {call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
</select>

「posts」コレクションが「posts」という名前の結果セットに含まれるデータから埋められることを指定します。

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">
    <id property="id" column="id"/>
    <result property="subject" column="subject"/>
    <result property="body" column="body"/>
  </collection>
</resultMap>

注意 マッピングする関連付けとコレクションの深さ、幅、組み合わせに制限はありません。マッピングするときはパフォーマンスに注意する必要があります。アプリケーションの単体テストとパフォーマンステストは、アプリケーションに最適なアプローチを見つける上で大いに役立ちます。良い点は、MyBatisを使用すると、コードにほとんど(またはまったく)影響を与えることなく、後で考えを変えることができることです。

高度な関連付けとコレクションのマッピングは、奥深いテーマです。ドキュメントだけでは限界があります。少し練習すれば、すぐにすべてが明らかになります。

discriminator

<discriminator javaType="int" column="draft">
  <case value="1" resultType="DraftPost"/>
</discriminator>

単一のデータベースクエリが、多くの異なる(ただし、できれば多少関連性のある)データ型の結果セットを返す場合があります。discriminator要素は、この状況や、クラス継承階層を含むその他の状況に対処するために設計されました。discriminatorはJavaのswitch文のように動作するため、理解するのは非常に簡単です。

discriminator定義は、column属性とjavaType属性を指定します。columnは、比較する値をMyBatisが探す場所です。javaTypeは、適切な種類の等価性テストが実行されるようにするために必要です(Stringはほとんどの場合、どのような状況でも機能する可能性がありますが)。例:

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultMap="carResult"/>
    <case value="2" resultMap="truckResult"/>
    <case value="3" resultMap="vanResult"/>
    <case value="4" resultMap="suvResult"/>
  </discriminator>
</resultMap>

この例では、MyBatisは結果セットから各レコードを取得し、その車両タイプの値を比較します。いずれかのdiscriminatorケースに一致する場合は、ケースで指定されたresultMapを使用します。これは排他的に行われるため、言い換えれば、残りのresultMapは(後で説明する拡張されていない限り)無視されます。いずれのケースも一致しない場合、MyBatisはdiscriminatorブロックの外で定義されたresultMapを単純に使用します。したがって、carResultが次のように宣言された場合

<resultMap id="carResult" type="Car">
  <result property="doorCount" column="door_count" />
</resultMap>

ドアカウントプロパティのみがロードされます。これは、親のresultMapに関係のない場合でも、完全に独立したdiscriminatorケースのグループを許可するために行われます。この場合、CarがVehicleであるため、車と車両の間に関係があることはもちろんわかっています。したがって、残りのプロパティもロードする必要があります。resultMapに1つの簡単な変更を加えるだけで、準備完了です。

<resultMap id="carResult" type="Car" extends="vehicleResult">
  <result property="doorCount" column="door_count" />
</resultMap>

これで、vehicleResultとcarResultの両方のすべてのプロパティがロードされます。

ただし、一部の人は、マップのこの外部定義がやや面倒だと感じるかもしれません。そのため、より簡潔なマッピングスタイルを好む人のための代替構文があります。例:

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultType="carResult">
      <result property="doorCount" column="door_count" />
    </case>
    <case value="2" resultType="truckResult">
      <result property="boxSize" column="box_size" />
      <result property="extendedCab" column="extended_cab" />
    </case>
    <case value="3" resultType="vanResult">
      <result property="powerSlidingDoor" column="power_sliding_door" />
    </case>
    <case value="4" resultType="suvResult">
      <result property="allWheelDrive" column="all_wheel_drive" />
    </case>
  </discriminator>
</resultMap>

注意 これらはすべてResult Mapsであり、結果をまったく指定しない場合、MyBatisは自動的に列とプロパティを一致させることを覚えておいてください。したがって、これらの例のほとんどは、実際よりも冗長です。とは言うものの、ほとんどのデータベースはやや複雑であり、すべてのケースでそれに依存できる可能性は低いでしょう。

自動マッピング

前のセクションですでにご覧いただいたように、簡単なケースではMyBatisが結果を自動マッピングできますが、他のケースでは結果マップを構築する必要があります。しかし、このセクションでわかるように、両方の戦略を組み合わせることもできます。自動マッピングの仕組みを詳しく見てみましょう。

自動マッピングの結果、MyBatisは列名を取得し、大文字と小文字を区別せずに同じ名前のプロパティを探します。つまり、IDという名前の列とidという名前のプロパティが見つかった場合、MyBatisはidプロパティにID列の値を設定します。

通常、データベースの列には大文字と単語間のアンダースコアを使用して名前が付けられ、Javaプロパティは多くの場合キャメルケースの名前付け規則に従います。それらの間の自動マッピングを有効にするには、設定mapUnderscoreToCamelCaseをtrueに設定します。

自動マッピングは、特定のresult mapがある場合でも機能します。この場合、各result mapについて、手動マッピングがないResultSetに存在するすべての列が自動マッピングされ、その後手動マッピングが処理されます。次のサンプルでは、iduserName列が自動マッピングされ、hashed_password列がマッピングされます。

<select id="selectUsers" resultMap="userResultMap">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password
  from some_table
  where id = #{id}
</select>
<resultMap id="userResultMap" type="User">
  <result property="password" column="hashed_password"/>
</resultMap>

自動マッピングレベルは3つあります。

  • NONE - 自動マッピングを無効にします。手動でマッピングされたプロパティのみが設定されます。
  • PARTIAL - ネストされた結果マッピングが内部(結合)で定義されているものを除き、結果を自動マッピングします。
  • FULL - すべてを自動マッピングします。

デフォルト値はPARTIALであり、それには理由があります。FULLを使用すると、結合結果の処理時に自動マッピングが実行され、結合は同じ行にいくつかの異なるエンティティのデータを取得するため、望ましくないマッピングになる可能性があります。リスクを理解するには、次のサンプルをご覧ください。

<select id="selectBlog" resultMap="blogResult">
  select
    B.id,
    B.title,
    A.username,
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>
<resultMap id="blogResult" type="Blog">
  <association property="author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
  <result property="username" column="author_username"/>
</resultMap>

この結果マップでは、BlogAuthorの両方が自動マッピングされます。ただし、Authorにはidプロパティがあり、ResultSetにidという名前の列があるため、AuthorのidにはBlogのidが設定されます。これは期待していたものではありません。したがって、FULLオプションは注意して使用してください。

構成された自動マッピングレベルに関係なく、属性autoMappingを追加することにより、特定のResultMapの自動マッピングを有効または無効にすることができます。

<resultMap id="userResultMap" type="User" autoMapping="false">
  <result property="password" column="hashed_password"/>
</resultMap>

キャッシュ

MyBatisには、非常に構成可能でカスタマイズ可能な強力なトランザクションクエリキャッシング機能が含まれています。MyBatis 3のキャッシュ実装では、より強力で構成がはるかに簡単になるように多くの変更が行われました。

デフォルトでは、ローカルセッションキャッシングのみが有効になり、これはセッションの期間中にデータをキャッシュするためにのみ使用されます。グローバルな第2レベルのキャッシュを有効にするには、SQLマッピングファイルに1行追加するだけで済みます。

<cache/>

文字通りそれだけです。この1つの単純なステートメントの効果は次のとおりです。

  • マップされたステートメントファイル内のselectステートメントからのすべての結果がキャッシュされます。
  • マップされたステートメントファイル内のすべてのinsert、update、deleteステートメントは、キャッシュをフラッシュします。
  • キャッシュは、削除にLeast Recently Used(LRU)アルゴリズムを使用します。
  • キャッシュは、時間ベースのスケジュール(つまり、フラッシュ間隔なし)でフラッシュされません。
  • キャッシュは、リストまたはオブジェクトへの1024個の参照を保存します(クエリメソッドが返すもの)。
  • キャッシュは読み取り/書き込みキャッシュとして扱われます。つまり、取得したオブジェクトは共有されず、他の呼び出し元またはスレッドによる他の潜在的な変更を妨げることなく、呼び出し元によって安全に変更できます。

注意 キャッシュは、キャッシュタグが配置されているマッピングファイルで宣言されたステートメントにのみ適用されます。XMLマッピングファイルと組み合わせてJava APIを使用している場合、コンパニオンインターフェースで宣言されたステートメントはデフォルトではキャッシュされません。@CacheNamespaceRefアノテーションを使用してキャッシュ領域を参照する必要があります。

これらのプロパティはすべて、キャッシュ要素の属性を通じて変更可能です。例:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

このより高度な設定では、60秒ごとにフラッシュし、結果オブジェクトまたはリストへの参照を最大512個まで格納し、返されるオブジェクトは読み取り専用と見なされるFIFOキャッシュを作成します。したがって、それらを変更すると、異なるスレッドの呼び出し元間で競合が発生する可能性があります。

利用可能な削除ポリシーは次のとおりです。

  • LRU – Least Recently Used (最近最も使用されていない): 最も長い間使用されていないオブジェクトを削除します。
  • FIFO – First In First Out (先入れ先出し): オブジェクトがキャッシュに入ってきた順序で削除します。
  • SOFT – Soft Reference (ソフト参照): ガベージコレクターの状態とソフト参照のルールに基づいてオブジェクトを削除します。
  • WEAK – Weak Reference (弱参照): ガベージコレクターの状態と弱参照のルールに基づいて、より積極的にオブジェクトを削除します。

デフォルトはLRUです。

flushIntervalは任意の正の整数に設定でき、ミリ秒単位で指定された妥当な時間を表す必要があります。デフォルトは設定されておらず、したがってフラッシュ間隔は使用されず、キャッシュはステートメントの呼び出しによってのみフラッシュされます。

サイズは任意の正の整数に設定できます。キャッシュするオブジェクトのサイズと環境の利用可能なメモリリソースを念頭に置いてください。デフォルトは1024です。

readOnly属性は、trueまたはfalseに設定できます。読み取り専用キャッシュは、キャッシュされたオブジェクトの同じインスタンスをすべての呼び出し元に返します。したがって、そのようなオブジェクトは変更しないでください。これにより、パフォーマンスが大幅に向上します。読み取り/書き込みキャッシュは、キャッシュされたオブジェクトのコピー(シリアル化による)を返します。これは遅くなりますが、より安全であるため、デフォルトはfalseです。

注意 セカンドレベルキャッシュはトランザクションです。つまり、SqlSessionがコミットで終了したとき、またはロールバックで終了したがflushCache=trueのインサート/削除/更新が実行されなかった場合に更新されます。

カスタムキャッシュの使用

これらの方法でキャッシュをカスタマイズすることに加えて、独自のキャッシュを実装したり、他のサードパーティのキャッシングソリューションへのアダプターを作成したりすることで、キャッシュの動作を完全にオーバーライドすることもできます。

<cache type="com.domain.something.MyCustomCache"/>

この例は、カスタムキャッシュの実装を使用する方法を示しています。type属性で指定されたクラスは、org.apache.ibatis.cache.Cacheインターフェースを実装し、String idを引数として取得するコンストラクターを提供する必要があります。このインターフェースは、MyBatisフレームワークの中でもより複雑なものの一つですが、その機能を考えると単純です。

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}

キャッシュを構成するには、Cache実装にpublic JavaBeansプロパティを追加し、キャッシュ要素を介してプロパティを渡します。たとえば、以下はCache実装でsetCacheFile(String file)というメソッドを呼び出します

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

すべての単純な型のJavaBeansプロパティを使用できます。MyBatisは変換を行います。また、構成プロパティで定義された値を置き換えるためのプレースホルダー(例:${cache.file})を指定できます。

3.4.2以降、MyBatisはすべてのプロパティが設定された後に初期化メソッドを呼び出すことをサポートしています。この機能を使用する場合は、カスタムキャッシュクラスでorg.apache.ibatis.builder.InitializingObjectインターフェースを実装してください。

public interface InitializingObject {
  void initialize() throws Exception;
}

注意 上記のセクションのキャッシュの設定(削除戦略、読み取り/書き込みなど)は、カスタムキャッシュを使用する場合は適用されません。

キャッシュ構成とキャッシュインスタンスは、SQLマップファイルのネームスペースにバインドされていることを覚えておくことが重要です。したがって、キャッシュと同じネームスペース内のすべてのステートメントは、それによってバインドされます。ステートメントは、ステートメントごとに2つの単純な属性を使用して、キャッシュとの対話方法を変更したり、完全に除外したりできます。デフォルトでは、ステートメントは次のように構成されています

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

これはデフォルトであるため、明らかにステートメントを明示的にそのように構成する必要はありません。代わりに、デフォルトの動作を変更する場合にのみ、flushCacheおよびuseCache属性を設定してください。たとえば、特定selectステートメントの結果をキャッシュから除外したり、selectステートメントにキャッシュをフラッシュさせたりする場合があります。同様に、実行時にキャッシュをフラッシュする必要のないupdateステートメントがある場合があります。

cache-ref

前のセクションから、この特定のネームスペースのキャッシュのみが、同じネームスペース内のステートメントで使用またはフラッシュされることを思い出してください。ネームスペース間で同じキャッシュ構成とインスタンスを共有したい場合があるかもしれません。そのような場合は、cache-ref要素を使用して別のキャッシュを参照できます。

<cache-ref namespace="com.someone.application.data.SomeMapper"/>