見出し画像

【SpringBoot+Docker】Web MVC ベース その⑤ ~一覧の検索と動的 JPQL の実装方法~

前回はコントローラーとテンプレートの両方で使える便利なヘルパーコンポーネントをコントローラーとテンプレートの両方で必要になるパスを一元管理する例で紹介しました。
model を介して受け渡す必要もなく使い勝手が良いと思います。
活用して下さい。

今回は一覧を検索する実装の方法と JPQL を動的に作成する JPQL ビルダーを紹介します。
一覧の検索条件によって JPQL を動的に作りたいということはよくあります。
ただ、こんな文字列を連結するようなプログラムを書くのは嫌です。

    String from = "FROM User ";
    
    String conditions = "";

    if (username != null && 0 < username.length) {
      conditions += "username = :username AND ";
    }

    conditions += "enabled = :enabled ";

    String where = "WHERE " + conditions;

    String order = "ORDER BY id ";

    String jpql = from + where + order;    

動的に JPQL を作ろうと思うと Criteria API があがりますが、あまり使われているのを見たことがありません。
おそらく可読性が低かったり、学習コストが高いのでしょう。
当社でも使用していません。
代わりに次のような JPQL ビルダーを開発して使用しています。

    JpqlBuilder<User> builder = JpqlBuilder.create(User.class);

    if (isPresent(username)) {
      builder.where("username LIKE :username", containing(username));
    }

    builder.where("enabled = :enabled", enabled);

    builder.order("id");

    String jqpl = builder.toJpql();

型安全の恩恵は得られませんが、文字列は最小限に抑えられ、手作業で文字列を連結する必要がなくなります。

今回もソースコードを用意しています。
含まれている DDL を使ってデータベースを作成する必要があります。
実際に動かしてみながら読んでください。

この記事は Java、Docker、Visual Studio Code(以下、vscode)、シェルなどのプログラミングに関する基本的な技術を知っている、または、調べれば分かる程度の知識がある前提で書いています。
分かりづらいところがあったらコメントでお知らせ下さい。

今日のノウハウ

一覧を検索するときの DTO の扱い

JPQL を動的に生成する JPQL ビルダーを作る

今回もソースコードを添付していますので、詳しくはそちらを見て下さい。
記事では構造やポイントを説明します。

一覧を検索する実装をするとき検索条件を入れておく DTO を準備します。
サンプルでは UserSearchForm です。
新規登録や編集で UserForm という DTO を使いましたがそれと同じようなものです。
ただ、UserSearchForm を使うときはコントローラーのメソッド引数に含めるだけでテンプレートで使えるようになり、パラメータを受け取ることもできるようになります。
そのあたりを Spring Boot がうまくやってくれます。
そこが実装のポイントになります。

JPQL ビルダーは動的に JPQL を生成する適当なツールがなかったので開発しました。
API のアイデアは Ruby on Rails から頂いています。
ノウハウを公開するわけにはいきませんので、サンプルプログラムに含まれているものはサブセット版です。
構造と拡張ポイントを説明したいと思います。

一覧を検索する

一覧を検索するときの実装方法です。
新規登録や編集画面と基本は同じなので簡単に説明します。

まずは UsersSearchForm です。
入力された検索条件を入れておく DTO です。
こんなふうに作ります。

@Data
@NoArgsConstructor
public class UsersSearchForm {      // ユーザー検索条件フォーム

  private String query;             // 検索文字列。ユーザー名を部分一致検索する。

  private boolean enabled = true;   // 有効か、無効か。

}

次に、UsersController の indexPage() メソッドで UsersSearchForm を引数で受け取るようにします。
また、サービスも UsersSearchForm に対応させたものを呼び出します。

@Controller                                                         // コントローラーとして扱う
@RequiredArgsConstructor                                            // 必須フィールドのコンストラクターを生成
public class UsersController {

  ※省略

  @GetMapping("#{usersHelper.indexPageMappingPath()}")                      // /users にマッピング
  public String indexPage(UsersSearchForm usersSearchForm, Model model) {   // usersSearchForm はクエリを受け取る役、クエリがなければ初期値。
                                                                            // model はビューへオブジェクト受け渡す役
    List<User> userList = usersService.search(usersSearchForm);             // ユーザーを検索し、user リストを取得する。
    model.addAttribute(userList);                                           // user リストをビューに渡す。userList でアクセスできる。コレクションは先頭小文字のクラス名+List
    return usersHelper.indexPageTemplate();                                 // template を指定する
  }

ここでよく見てほしいのですが、usersSearchFormを model に addAttribute() していません。
Spring Boot が引数に指定した DTO を自動的に model attribute に追加してくれているからです。
引数にあることで Post データやクエリデータの値を自動的にマッピングもしてくれます。
一覧の検索はここが実装のポイントです。

あとは新規画面などと同じですので省略します。
index.html や UserService などを合わせて修正しています。
ソースコードを参照して下さい。

JPQL ビルダーを作る

次に JPQL ビルダーの説明です。
使用している UserRepository から説明します。

@Repository                                       // レポジトリとして扱う
@RequiredArgsConstructor                          // 必須フィールドのコンストラクターを生成
public class UserRepository implements QueryUtil {                     // 公開 User リポジトリ

  ※省略

  private final EntityManager entityManager;      // エンティティマネージャーを注入

  ※省略

  public List<User> findBy(UsersSearchForm usersSearchForm) {                         // 検索フォームの条件で user リストを取得する。
    JpqlBuilder<User> builder = JpqlBuilder.create(User.class);                       // JPQL ビルダーを生成。FORM 句のエンティティを指定する。

    if (isPresent(usersSearchForm.getQuery())) {
      builder.where("username LIKE :username", containing(usersSearchForm.getQuery()));  // 条件を指定
    }

    builder.where("enabled = :enabled", usersSearchForm.isEnabled());                 // 条件を追加(AND になる)

    builder.order("id");                                                              // ORDER 句にカラム名を追加する

    TypedQuery<User> query = createTypedQuery(entityManager, builder);                // TypedQuery を生成
    return query.getResultList();
  }      

entityManager を注入しています。
これはあとで TypedQuery を生成するときに必要です。

findBy() メソッドのはじめに JpqlBuilder を生成します。
create() メソッドに指定しているのは FROM 句に指定するエンティティの Class です。

さっそく、isPresent() メソッドで値の存在をチェックし、値が存在すれば where() メソッドで条件式を指定しています。
続く where("enable …) は無条件に指定している条件式です。
それぞれ、パラメータ値も一緒に指定しています。

ORDER 句の指定もカラム名だけを指定しています。

最後に用意したユーティリティメソッドの createTypedQuery() メソッドで JPQL の実行に必要な TypedQuery を生成して、User リストを取得して返しています。

少し慣れが必要ですが、JPQL の構造に近くて、読みやすく使いやすいと思います。
文字列は必要最小限の指定になっています。
isPresent() や containing() などのユーティリティメソッドのおかげで統一した方法で動的で簡潔な記述ができています。

JpqlBuilder

実装はそれぞれシンプルなので省略します。
これが JPQL ビルダー本体です。
サブセット版ですので、SELECT 文に特化しています。

public class JpqlBuilder<T> implements ToJpql {                   // JPQL ビルダー本体。

  private final From<T> from;                                     // FROM 句。
  private final Conditions where;                                 // WHERE 句。
  private final Order order = new Order();                        // ORDER BY 句。

  private final Map<String, Object> parameters = new HashMap<>(); // パラメータのキーと値を Map で管理する。

  private JpqlBuilder(From<T> from) {
    ※省略

  public static <T> JpqlBuilder<T> create(Class<T> fromEntity) {  // ビルダー生成
    ※省略

  public Conditions where(String condition, Object... args) {     // WHERE 条件を追加
    ※省略

  public Order order(String column) {                             // ORDER カラムを追加
    ※省略

  public Set<Entry<String, Object>> getParameters() {             // パラメーターのセットを取得する。
    ※省略

  public Class<T> getFromEntity() {                               // FROM 句のエンティティ Class を取得する。
    ※省略

  @Override
  public String toJpql() {                                        // JPQL を生成する。
    ※省略

フィールドの from、where、order がそれぞれの句に対応しています。
ここでは from とwhere、order を操作するメソッドと Query を作成するのに必要なメソッドが並びます。
JOIN や GROUP BY などに対応する場合もここにフィールドとメソッドを追加することになります。

where は Conditions クラスになっていますが、Where クラスにしていないのは JOIN の ON 句や OR 句を表現するのにも使えるからです。

parameters は where() で指定したパラメータのキーと値を管理します。
パラメータのキーと値はここに集約します。

From

FROM 句に対応するクラスです。
FROM 句に指定するエンティティの Class を管理します。

public class From<T> implements ToJpql {            // FROM 句に対応する。

  private final Class<T> entity;                    // エンティティの Class。取得するリストの型になる。

  From(Class<T> entity) {
    ※省略

  public Class<T> getEntity() {
    ※省略

  @Override
  public String toJpql() {                          // JPQL を返す。
    ※省略

JOIN 句に対応するとエイリアスを指定したくなると思います。

Conditions および Condition

WHERE 句に対応するクラスです。
Where クラスではなく Conditions クラスにしてあるのは JOIN の ON 句や OR 句などの表現でも使えるからです。

public class Conditions implements ToJpql {                                 // 複数の条件式を表すクラス。WHERE に対応する。

  private final Map<String, Object> parameters;                             // パラメータ文字列と値を Map で管理する。
  private final List<Condition> conditions = new ArrayList<>();             // 条件のリスト

  Conditions(Map<String, Object> parameters) {
    ※省略

  public Conditions add(String condition, Object... values) {               // 指定された条件を条件のリストに追加
    ※省略

  @Override
  public String toJpql() {                                                  // JPQL を返す。
    ※省略

parameters は JpqlBuilder の parameters を参照させます。

条件式を表す Condition を List で管理します。
toJpql() で管理している条件式を AND で結合します。

Conditions を Condition として扱うことができるようにして、プリフィックスやデリミターをカスタマイズできるようにすると JOIN の ON 句や OR にも対応できるようになります。

実際の条件を表すのが Condition です。

public class Condition implements ToJpql {                                                // 条件式を表すクラス。WHERE 句の各条件に対応する。

  private static final Pattern PATTERN = Pattern.compile(":(\\w+)");                      // パラメータを表す正規表現パターン。

  private final String condition;                                                         // "column = :param" など。

  public Condition(Map<String, Object> parameters, String condition, Object... values) {  // arguments は指定されたパラメータと値を保存する。
    ※省略

  @Override
  public String toJpql() {
    ※省略

指定された条件式を管理するだけのクラスです。
コンストラクタの中でパラメータを処理しているのであまり良い作りではないのですが、実態はこれで十分です。

Order

ORDER 句に対応します。
カラム名を管理します。

public class Order implements ToJpql {                              // ORDER 句に対応する。

  private final List<String> orders = new ArrayList<>();            // カラム名を List で管理する。

  Order() {}

  public Order add(String column) {                                 // カラム名を List に追加
    ※省略

  @Override
  public String toJpql() {                                          // JPQL を返す。
    ※省略

ASC、DESC を指定できるようにするといいでしょう。

Function

JPQL ビルダーを簡潔に使えるようにするためのユーティリティメソッドです。
メソッドを static import して使っています。
好みで拡張して下さい。

まとめ

一覧を検索する実装は、検索条件を入れておく DTO を indexPage() の引数に含めることで model の attribute にしつつ、クエリーデータを DTO にマッピングさせるところがポイントです。
JPQL ビルダーは動的な JPQL を簡潔に実装できるシンプルな実装を紹介しました。
JOIN、OR、IN、EXISTS など足りない機能はたくさんありますが、これをベースに拡張してもらえたらと思います。
今後の記事でも必要なときに拡張していきます。

次回の記事では、検索条件を保存しておく方法を紹介します。
今の実装では一覧に戻るときに検索条件が消えてしまいます。
Redis も導入して業務アプリケーションに近づけていきます。


この記事が気に入ったらサポートをしてみませんか?