1
0

More than 3 years have passed since last update.

JdbcTemplateで@OneToMany的な振る舞いをさせる

Last updated at Posted at 2020-03-16

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0