前回の記事でヘッダ明細型のデータをMyBatisで読み込む方法としてcollectionタグを使いネストしたJavaオブジェクトに読み込む方法を紹介した。ヘッダテーブルと明細テーブルを結合した1つのSQLで読み込みができるため楽な方法ではあるが使い方に癖があるようなので注意しないと思わぬところではまってしまう。その一つが行数を指定して読み込むときである。
例として以下のようなデータをcollectionタグを使いネストしたJavaオブジェクトで読み込んでみよう。
studentテーブル(ヘッダ)
id |
name |
100 |
やまだ |
101 |
たなか |
102 |
おばた |
103 |
ささおか |
resultテーブル(明細)
refid |
course |
score |
100 |
英語 |
80 |
100 |
数学 |
90 |
101 |
英語 |
50 |
101 |
数学 |
68 |
102 |
英語 |
77 |
102 |
数学 |
70 |
103 |
英語 |
98 |
103 |
数学 |
92 |
読み込むオブジェクトのJavaクラスの定義とMyBasisのmapperは前回の記事と同じものを使用する。
Javaクラス定義
public class Student {
int id;
String name;
Result[] result;
}
public class Result {
String course;
int score;
}
MyBasisの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>
<select id="find" resultmap="student">
SELECT id, name, course, score
FROM student, result
WHERE id = refid
ORDER BY id
</select>
</mapper>
IDの小さい順に2人、やまだ君(100)、たなか君(101)、のデータだけ読み込むことを考えてみる。MyBatisではRowBoundsを使うことで指定した行範囲のデータを読み込むことができるので、以下のコードで0行目から2行の読み込みをしようとした。
// 読み込む行範囲を指定、先頭から2行のデータを読み込む
int offset = 0;
int limit = 2;
RowBounds rowBounds = new RowBounds(offset, limit);
// SQLに渡すパラメータ(特に無し)
Map<String,Object> params = new HashMap();
// SQLを発行してオブジェクトにマッピングする
List<Student> students = sqlSession.selectList("mapper.student.find", params, rowBounds);
しかしcollectionを使っている場合はそんなに単純ではない。RowBoundsへの期待はstudentテーブルの先頭2行だけであるが、SELECTの結果は以下のようにstudentの内容とresultの内容が1行に結合されたものとなり、RowBoundsはその先頭2行を対象としてしまうようである。結果的にstudentsには、やまだ君のデータだけが読み込まれる。
SELECTの結果
id |
name |
course |
score |
RowBoundで選ばれる行 |
100 |
やまだ |
英語 |
80 |
〇 |
100 |
やまだ |
数学 |
90 |
〇 |
101 |
たなか |
英語 |
50 |
× |
101 |
たなか |
数学 |
68 |
× |
102 |
おばた |
英語 |
77 |
× |
102 |
おばた |
数学 |
70 |
× |
103 |
ささおか |
英語 |
98 |
× |
103 |
ささおか |
数学 |
92 |
× |
これを回避するにはRowBoundsを使わず、全行読み込んだ後にoffsetとlimitでリストを切り取る処理を行っている。
一旦全行読み込んで処理をするのでメモリ的にもCPU的にも負荷がかかるのが欠点ではあるが、それほどの規模でなければコードの読みやすさでこの方法を使っている。
ただ、行数が多いテーブルではSQLの書き方も工夫するなど何らかの対策が必要がと思われる。
// 読み込む行範囲を指定、先頭から2行のデータを読み込む
int offset = 0;
int limit = 2;
// SQLを発行してオブジェクトにマッピングする
List<Student> students = sqlSession.selectList("mapper.student.find");
// 行範囲で切り取り
int s = offset;
int e = offset+limit;
if (s < 0) { s = 0; }
if (e >= students.size()) { e = students.size(); }
if (s == 0 && e >= students.size()) {
return students;
} else {
return students.subList(s, e);
}