Query By Exampleを使ってSQLを書かずに空欄無視検索


以下のように検索キーワード入力欄が多数存在した際に、コントローラー側でif文を使って場合分けをしようとするとかなり大変になる上、コードが汚くなります。そこで調べたところ、SpringJPAの「Query By Example」という機能を使うとSQLを書かなくても空欄のパラメーターを無視して検索することができるとのことだったので実装してみました。SQLを使ってもできたのですが、こっちの方がよりスマートかなと思ったので、こちらも備忘録として残しておきます。間違いがありましたら指摘いただけると助かりますorz

公式のドキュメントはこちらです。

以下のようなEntityがあったとして、その要素のどれでも検索できるような画面の検索欄があった際にはさすがに気合でif文をを使ってやるのは無理ですよね

@Getter
@Setter
public class Asset {
  private Integer id;
  private Integer categoryId;
  private String adminName;
  private String assetName;
  private String remarks;
  private String serialId;
  private String purchaseDate;
  private String makerName;
  private String accessory;
  private String storingPlace;
}

そこで以下のように、コントローラーとサービスを記述するとSpringのJPAが画面上で空欄のまま検索ボタンが押され、パラメータが空の検索条件を無視してレコード取っといてきてくれます

// Controller
@GetMapping("/search")
  public String search(@ModelAttribute("asset") Asset asset, Model model) {

    List<Asset> assets = assetService.search(asset);
    model.addAttribute("assets", assets);

    return "practice";
  }
//Service
public List<Asset> search(Asset asset) {
        ExampleMatcher customExampleMatcher = ExampleMatcher.matching()
                    .withMatcher("adminName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                    .withMatcher("assetName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                    .withMatcher("remarks", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                    .withMatcher("makerName", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                    .withMatcher("serialId", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                    .withMatcher("purchaseDate", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                    .withMatcher("accessory", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase())
                    .withMatcher("storingPlace", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase());

        Asset asset = new Asset(asset); // Asseクラスにこれ用のコンストラクタを用意
        Example<Asset> example = Example.of(asset, customExampleMatcher); // 検索条件を作成する
        List<Asset> assetList = assetRepos.findAll(example); // Repositoryには何も書かなくてOK!
        return assetList;

この中で特に学んだこととしては、
1. ExampleMatcher customExampleMatcher = ExampleMatcher.matching()でまたされたパラメータに一致する検査結果を持ってくれる。この時点でnullのパラメータは無視してくれる
2. .withMatcher()String型パラメータの検索条件をカスタマイズできる
3. ExampleMatcher.GenericPropertyMatchersの後に、メソッドを続けることで完全一致・部分一致/大文字小文字制限などが設定できる
- caseSensitive() = 大文字小文字の区別をつける
- ignoreCase() = 大文字小文字の区別をつけない
- contains() = 部分一致
- exact() = 完全一致
- endsWith() = 後方一致
- startsWith = 前方一致
こんな感じで、ExampleMatcherのインスタンスを作成してJPAに用意されているfindAll(Example<T>)メソッドを使えばOK。ちなみに、findAll(Example<T>, Pageable)メソッドもあるので、ページネーションと合わせることも結構簡単にできます。