📒

Spring Data JDBCでJOINした複数テーブルをページ・ソートする方法

 
こんにちは。Nstockエンジニアのryan5500です。
今回は、私たちが利用しているSpring Data JDBCというORMで、複数テーブルに対してページング・ソートを行う方法についてご紹介します。

忙しい方向けの要点

やり方は主に2つあります。
 
  • RepositoryImplを用意し自前実装する
    • 記事内でサンプルコードを用意
  • DBで複数テーブルをJOINしたViewを用意し、それに紐付けたEntity, Repositoryを作る
 

背景 & 想定読者

私たちNstockはバックエンドにてJava 17 / Spring Boot2系 / PostgreSQLを利用してSaaSを開発しており、ORMとしてSpring Data JDBCを利用しています。
本記事の想定読者はSpring Data JDBCを利用されている方です。そのため、Spring Data JDBCの個々の機能の詳細説明は省きます。

Spring Boot3系についての注意

私達はSpring Boot 2系を利用して開発していたのですが、3.0系から、本記事で書いているPagingAndSortingRepositoryが、CrudRepositoryに統合されました。
本記事の内容は引き続き利用できますが、Spring Boot 3.0以降をご利用の読者は、PagingAndSortingRepositoryをextendsしている箇所はCrudRepositoryで対応できると読み替えて頂けますと幸いです。
私達は現時点では3系を利用しています。

Spring Data JDBCについて

Spring Data JDBCを利用する中で、シンプルで学習コストが低いORMという印象を受けています。
利用している中で助かっている機能としては以下があります。
 
  • CRUD処理が簡単に導入できる
  • 特定の命名規則に従ったメソッド定義をinterfaceに追加すると、SQLクエリや実装を書かずに、メソッドとして利用できる
    • e.g. ,
  • 複雑な処理は アノテーションでSQLを書ける
    • それ用のEntityを用意すればJOINも書ける
    • 引数でプレースホルダーを渡せる
  • ページング・ソートに対応できるRepositoryのinterfaceが用意されているため、簡単に導入できる
 
開発の初期フェーズにおいてはシンプルなモデルが多かった関係で、特に苦労なく利用できており、必要十分という印象でした。

課題: JOINしたテーブルへ、ページングを実現したい

しかし、開発を進めていく中で、対応が難しいケースに遭遇しました。それは、「JOINしたテーブルへ、ページングを実現したい」というものです。
具体例を上げると、以下のようなケースになります。
要件: 「 テーブルの一覧に テーブルの結果を混ぜ込んで表示したい。さらにページングもしたい。」
 
テーブルのイメージ
idnameaddress
1Alice1-2-3 some blvd
2Bob2-1 where street
テーブルのイメージ
idmember_idpurchase_total_num
1110
2220
実現したい表(各カラムでソート & ページングしたい)
memberテーブルのnamemember_purchase_summaryテーブルのpurchase_total_num
Alice10
Bob20
 
解決策として、いくつかアイデアを検討しました。

@Queryアノテーションで書ける?

アノテーションでSQLクエリが書けるので、それを応用すると 型のオブジェクトを引数に取る関数を用意し、Queryアノテーション内で展開できるのではないかと考えました。
型は、ページングとソートに必要な情報をまとめたデータ構造と言えそうです。
具体的には、ページングのために表示するページ数と1ページあたりのサイズ を、ソートのためにソート対象カラムと降順・昇順を複数持つ 型を保持しています。
を利用する場合は以下のようなコードで利用できます。
 
アノテーションで書くというのは、以下のコードは実際には動きませんが、Repository側の実装はこのようになるイメージでした。
 
しかし アノテーションで引数利用できるのはプリミティブな型のみであり、 型の引数の中身を取り出してSQLクエリに差し込むことは難しいようでした。

PagingAndSortingRepositoryでなんとかできる?

単一のテーブルに対して、ページングやソートを提供する があるので、このクラスのオプションでテーブルのJOINが実現できないかを調査しました。
ただ、オプションで対応する方法を見つけることはできませんでした。

JOINしたテーブルのページングだけ別のORMを使う?

Spring Data JDBC以外のORM、例えばSpring Data JPAを導入すれば対応できるのではないかという方法です。
ですが、複数のORMを1アプリ内に同居させるのは認知負荷が高くなりそうなので、採用しませんでした。

解決策

解決策は以下の2つありそうです。
 
  1. DBのViewを利用する
  1. RepositoryImplクラスを用意しカスタムクエリを書く
 

DBのViewを利用する

JOINしたテーブルを、DBのViewとして定義すれば、1テーブルとして表現できます。これを応用すると、JOINしたテーブルを アノテーションでEntityに対応付けることができます。
そのEntityの結果を返すRepositoryは、 を拡張すれば、ページングやソートを実現できます。
具体的には以下です。
 
この方式のメリットは、Spring Data JDBCに用意されている を利用するので、困ったときの検索時にも情報を得やすく、認知負荷が小さくなりそうな点です。
一方デメリットは、Viewが利用できるかは各社の管理ポリシーによるため、常に選択肢として利用可能ではないことが想定されます。他にも、ページングを利用したいJOINテーブルごとにビューが増えてしまう点があります。
私たちもこの方法の動作検証はしたものの、Viewに対するRLSの対応状況から、この解決策を取ることはできませんでした。
具体的には、私達はサービスでRLSを利用しているのですが、当時利用していたPostgreSQLのv14ではViewに対してクエリする際、Viewの中で利用しているテーブルに対してRLSが効いている状態でクエリすることができなかったためです。
PostgreSQLのv15以降ではsecurity_invokerを設定することで利用できます。

RepositoryImplクラスを用意しカスタムクエリを書く

他の解決策を調べたところ、RepositoryImplクラスを用意するとカスタムクエリを書けることがわかりました。
これを用いて、JOINしたテーブルに対して 型のオブジェクトを引数に取って処理できるように実装を作りました。
一部コードを修正していますが、イメージとしては以下のようにコードを書きました。

感想

ありそうなユースケースに思うのですが、やってみると意外と関連する情報が見当たらなかったことが、記事にする動機になりました。
に似せて を引数に取るようにすることで、ソート条件とページングの条件をまとめることができたのは、認知負荷を下げる意味でよかったように思います。
もしもっと良い方法があるよ!などあればぜひお知らせいただければ幸いです。

まとめ

Spring Data JDBCでJOINしたテーブルにページングするには、解決策が2種類ありそう
 
  • DBのビューを利用すること
  • Implでカスタムクエリを作ること
 

 
Nstockではエンジニアを募集しています!
👨‍👩‍👧‍👦カジュアル面談から気軽にお話しましょう🤞