TL;DR
ORMにを頼らずNamedParameterJdbcTemplateとRowMapperを組み合わせて結果を取得するシステムでOneToManyなデータの持ち方を実現したい場合はResultSetExtractorを実装したクラスを作ればいいが、構造が複雑になりがちなので新しいシステムを作る時は素直にJPA等に頼ったほうが良い
問題点
Spring JDBCには NamedParameterJdbcTemplate
等のクラスをDIすることで気軽にクエリを発行できる仕組みがある。
@Repository
public class HogeRepository {
@Resource
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Override
public HogeWithPiyo findAll() {
return namedParameterJdbcTemplate.query("SELECT * FROM hoge LEFT OUTER JOIN piyo USING(hoge_id)",
new BeanPropertyRowMapper<>(HogeWithPiyo.class));
}
}
public class HogeWithPiyo {
Integer hogeId;
List<Piyo> piyoList;
public Integer getHogeId() {
return hogeId;
}
public void setHogeId(Integer hogeId) {
this.hogeId = hogeId;
}
public List<Piyo> getPiyoList() {
return piyoList;
}
public void setPiyoList(List<Piyo> piyoList) {
this.piyoList = piyoList;
}
public class Piyo {
Integer piyoId;
public Integer getPiyoId() {
return piyoId;
}
public void setPiyoId(Integer piyoId) {
this.piyoId = piyoId;
}
}
問題
例えば、 hoge
, piyo
テーブルに対して結果セットが次のように帰ってくるとする。
hoge.hoge_id | piyo.hoge_id | piyo.piyo_id |
---|---|---|
1 | 1 | 1 |
1 | 1 | 2 |
この時、BeanPropertyRowMapper等のRowMapper系でデータを取得するしかないシステムで Hogeリストとそれに紐づくPiyoリスト
を取得するために、ビジネスロジックでPiyoを取得するためにクエリを発行しているがそもそものHogeの件数自体がめちゃくちゃ多いので件数分クエリを発行するとスループットがめちゃくちゃ遅い、という問題があったと仮定する。BeanPropertyRowMapperを使うとどうしても1行ずつ読んでしまうため、結果は2行のリストになってしまう。
public class HogeExtractor<T extends Hoge> implements ResultSetExtractor<List<T>> {
BeanPropertyRowMapper<T> parentRowMapper;
BeanPropertyRowMapper<Piyo> childRowMapper = new BeanPropertyRowMapper<>(Piyo.class);
String uniqueColumn = "hoge_id";
String childKey = "piyo_id";
@SafeVarargs
public HogeExtractor(T... e) {
@SuppressWarnings("unchecked")
Class<T> type = (Class<T>) e.getClass().getComponentType();
this.parentRowMapper = new BeanPropertyRowMapper<>(type);
}
@Override
public List<T> extractData(ResultSet rs) throws SQLException, DataAccessException {
List<T> rows = new ArrayList<>(); // 結果のリスト
List<Object> childs = null;
Long key = null;
T current = null;
int parentIdx = 0;
int childIdx = 0;
while (rs.next()) {
if (current == null || !key.equals(rs.getLong(uniqueColumn))) {
// 親テーブルのデータが無い時やJOINするカラムの値が変わったら親テーブルのオブジェクトを作る
key = rs.getLong(uniqueColumn);
current = parentRowMapper.mapRow(rs, parentIdx++);
childs = new ArrayList<>();
current.setPiyo(childs);
childIdx = 0;
rows.add(current);
}
// 毎行、小テーブルの要素を親テーブルのオブジェクトに追加する
// ただし、小テーブルのユニークキーがnull(1行もフェッチしてない)の場合は追加しない
if (rs.getObject(childKey) != null) {
childs.add(childRowMapper.mapRow(rs, itemIdx++));
}
}
return rows;
}
}
@Override
public HogeWithPiyo findAll() {
return namedParameterJdbcTemplate.query(
"SELECT * FROM hoge LEFT OUTER JOIN piyo USING(hoge_id)",
new HogeExtractor<>());
}
そんな時はこんな感じでHoge専用のResultSetExtractorを実装するクラスを無理やり作っちゃう。
注意
※ 取得するクエリは必ずjoinするキーごとに並んでいる必要があります
t1.col_id | t1.join_col | t2.col_value |
---|---|---|
a | 1 | x |
b | 2 | y |
c | 1 | z |
この様に帰ってくるクエリを書いちゃうと正しく結果をセットできないので、 join_col
でソートするかExtractorの結果セットをMap型で管理するよう変更するなどの工夫が必要です。
問題点
- 取得したいオブジェクトごとにResultSetExtractorを実装しなきゃいけないのでしんどい
- 継承したクラスならジェネリクス使えばなんとかなる
- リストの要素を追加するメソッド名をリフレクションで解決すればなんとなく1つのクラスで済む気がするけどしんどい
- OneToManyしたいフィールドが2箇所以上ある時とかもしんどい
- JOINするキーが2個以上ある時もしんどい
結論
- 既存の仕組みを守りつつ最小のコストでスループットを上げるためのテクニックとしてならアリなんじゃないかな……と思っています
- ちゃんとテストが書ける前提で負債を最小限にコントロールできるなら良いと思うけど、どちらかというと負債を作る行為
- 実運用した事は無いのでベンチマークはありません
参考にしました
java - ParameterizedRowMapper That Maps Object List to Object - Stack Overflow