2020年6月14日日曜日

MyBatisでヘッダ明細型のデータをINSERTする(自動採番もする)

MyBatisでヘッダ明細型のデータを扱うシリーズの3回目。今回はネストしたJavaオブジェクトをデータベースに新規作成する方法について書いてみたいと思う。またIDもデータベースで自動採番させようとちょっと欲張ったものにしている。 今回はSQLの都合上MySQLに特化した内容となっている。

今回のサンプルもいつもと同じ以下のクラスとテーブルを使用する。

public class Student {
  int id;
  String name;
  Result[] result;
}

public class Result {
  String course;
  int score;
}

studentテーブル(ヘッダ)

id name
100 やまだ

resultテーブル(明細)

refid course score
100 英語 80
100 数学 90

データベースはMySQLを使用する。 idは連番を振りたいのでデータベースで自動採番させてしまおう、ということで以下のようにテーブル定義をした。

CREATE TABLE student (
  id INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
  name VARCHAR(100)
);

CREATE TABLE result (
  refid INTEGER NOT NULL,
  course VARCHAR(100),
  score INTEGER
);

MyBatisのmapper定義

さて、ヘッダ明細型のテーブルに対してJavaオブジェクトをINSERTするMyBatisのmapperの定義をする。 INSERTの処理としてまず思いつくのは、MyBatisの設定でstudentテーブルとresultテーブルにINSERTするmapperをそれぞれ作成してJavaコードから順に呼び出す方法だろう。この場合はStudentクラスのオブジェクトをMyBatis経由で書き込んだ後、resultの配列ごとにループを回してResultクラスのオブジェクトをMyBatis経由で書き込む処理となる。

しかし、ネストしたStudentクラスのオブジェクトをMyBatisに渡して、その中でネストした部分も書き込んでもらうことはできないだろうかということで、以下のようなmapperを作成した。

<mapper namespace="mapper.student">
  <resultmap id="student" type="student">
    <id column="id" property="id" />
    <result column="name" property="name" />

    <collection property="result" oftype="Result">
      <result column="course" property="course" />
      <result column="score" property="score" />
    </collection>
  </resultmap>

  <insert id="create" parameterType="student" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO student (name) VALUES (#{name});
    SET @uid = LAST_INSERT_ID();
    <if test="result != null">
      <foreach item="i" collection="result">
        INSERT INTO result (refid, course, source)
          VALUES (@uid, #{i.course}, #{i.source});
      </foreach>
    </if>
    SELECT @uid FROM dual;
  </insert>

</mapper>

まずは最初の "INSERT INTO student ~" でStudentクラスのオブジェクトをstudentテーブルに書き込む。

次の行で "SET @uid = LAST_INSERT_ID()" を行っているが、ここでstudentテーブルに書き込んで自動採番されたIDを取得している。 これはMySQL専用の構文となる。

次にMyBatisの動的SQLを使ってネストしたオブジェクトを1つづつINSERTする。
まず、<if test="result != null"> でStudentクラスのオブジェクトがネストしたResultオブジェクトを持っているか(nullでないか)を調べ、持っているなら <foreach item="i" collection="result"> でひとつづつ順にINSERT処理をしている。
resultテーブルではrefid列に紐づくstudentテーブルのidを保持するが、先にMySQLの変数 "@uid" に自動採番された値を保存しているので、その値を書き込んでいる。

自動採番された値はJavaのStudentクラスのオブジェクトにも反映したい。MyBatisの処理結果として返した値が反映されるので、最後に "SELECT @uid FROM dual" を実行して、自動採番された値を返している。

Javaコードから書き込み

以下のようなJavaコードでネストされたオブジェクトを1回のMyBatis呼び出しでヘッダ明細型のテーブルにINSERTできる。

// 英語の結果
Result result_english = new Result();
result_english.course = "英語";
result_english.score = 50;

// 英語の結果
Result result_math = new Result();
result_math.course = "数学";
result_math.score = 68;

// 生徒の情報
// IDは自動採番されるため
Student student = new Student();
student.name = "たなか";
student.result = new Result[] { result_english, result_math };

// オブジェクトをデータベースに書き込む
sqlSession.insert("mapper.student.create", student);

// 自動採番された生徒のIDを表示
System.out.println("ID=" + student.id);

おわりに:BadSqlGrammarExceptionが出た時

Springから使っていたらこんな例外が出てしまいました。

org.springframework.jdbc.BadSqlGrammarException: 
### Error updating database.  Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SET @uid = LAST_INSERT_ID();
		 
		SELECT @uid FROM dual' at line 3

今回はMyBatisのmapperで複数のステートメントを一度に実行していますが、MySQLのJDBCドライバの仕様で複数ステートメントを実行するときは "allowMultiQueries=true" オプションを指定する必要があるようです。JDBC接続のURLに以下のようにオプションを追加したらうまく動くようになりました。


jdbc.url=jdbc:mysql://localhost/student?allowMultiQueries=true