Spring Data JPAのQBE(Query by Example)を使った検索のサンプルコード

# 概要

QBE(Query by Example：”例示による問い合わせ”)とは問い合わせ言語の1つで、下記にwikipediaの説明を引用させて頂きました。

> [例示による問い合わせ] (https://ja.wikipedia.org/wiki/%E4%BE%8B%E7%A4%BA%E3%81%AB%E3%82%88%E3%82%8B%E5%95%8F%E3%81%84%E5%90%88%E3%82%8F%E3%81%9B)
> 関係データベース (リレーショナルデータベース) 向けの問い合わせ言語の一つである。 QBEは、1970年代半ばにアメリカ合衆国ニューヨーク州ヨークタウンのIBM研究センターで、別の問い合わせ言語SQLの開発と並行して、モシェ・ズルーフが考案した。 QBEは、最初の視覚的な問い合わせ言語である。

Spring Data JPAではバージョン1.10よりサポートされていたので、簡単な検索処理のサンプルコードを書いてみました。

**環境**

* Windows10 Professional
* Java 1.8.0_144
* Spring Boot 1.5.6
* Spring Data JPA 1.11.6

**参考**

* [Spring Data JPA - Reference Documentation] (https://docs.spring.io/spring-data/jpa/docs/current/reference/html/)

# QBEで検索するには

QBEで問い合わせを行うには、Spring Data JPAのExample（Exampleというとサンプルコードなどを連想してしまいますが、org.springframework.data.domain.Exampleというクラスのことです）、ExampleMatcherを使用します。また[QueryByExampleExecutor] (https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/query/QueryByExampleExecutor.html)というインターフェースも使いますが、JpaRepositoryがこれを継承しているのでJpaRepositoryを利用していれば実装で改めて意識する必要はありません。

## サンプルコード

下記に簡単ですが、QBEで検索するサンプルコードを掲載します。QBEでは簡単な検索処理を簡潔に記述することができますが、複雑な条件での検索には向いていないように感じました。

### エンティティ

QBE検索のサンプルコードで使用するItemエンティティです。

```java
@Entity
@Table(name="item")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = {"itemStocks"})
@EqualsAndHashCode(exclude = {"itemStocks"})
public class Item implements Serializable {

    private static final long serialVersionUID = -3153084093423004609L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name="name", nullable = false)
    private String name;
    @Column(name="price", nullable = false)
    private Integer price;
    @Column(name="sales_from", nullable = false)
    private LocalDateTime salesFrom;
    @Column(name="sales_to", nullable = false)
    private LocalDateTime salesTo;
    @Enumerated(EnumType.ORDINAL)
    @Column(name="standard_type", nullable = false)
    private StandardType standardType;
    @JoinColumn(name = "category_id", nullable = false)
    @ManyToOne
    private Category category;
    @Column(name="del_flag", nullable = false)
    private Boolean delFlag;
    @Column(name="create_at", nullable = false)
    private LocalDateTime createAt;
    @Column(name="update_at", nullable = false)
    private LocalDateTime updateAt;

    @OneToMany(mappedBy = "item", cascade = CascadeType.ALL)
    private List<ItemStock> itemStocks;
}
```

### リポジトリ

ItemRepositoryです。特にQBE用のカスタムメソッドの実装は行っていません。
JpaRepositoryはQueryByExampleExecutorを継承しているので、このままでQBEでの検索が行えます。

```java
import com.example.domain.entity.Item;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ItemRepository extends JpaRepository<Item, Long> { }
```

ちなみに、QueryByExampleExecutorインターフェースで定義されているメソッドはfindOne, findAll, count, existsがあります。

### [Example] (https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Example.html)

もっとも簡単な検索は、検索条件をエンティティで表現し、それを使ってExampleインスタンスを生成し、リポジトリの検索メソッドへ渡す方法です。

このサンプルは、priceが1000、standardTypeがDのデータを検索します。
エンティティのプロパティにnull以外の値がセットされると検索条件として使用されます。

```java
@Autowired
private ItemRepository repository;

public void find() {

  // 検索条件をエンティティにセット
  Item probe = new Item();
  probe.setPrice(1000);
  probe.setStandardType(StandardType.D);

  // 検索条件からExampleインスタンスを生成
  Example<Item> example = Example.of(probe);

  List<Item> items = repository.findAll(example);
  items.forEach(System.out::println);
}
```

### [ExampleMatcher] (https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/ExampleMatcher.html)

もう少し複雑な条件で検索したい場合はExampleMatcherで検索条件を組み立てます。

#### matching / matchingAll

検索条件で指定するプロパティすべてが一致するデータを検索するExampleMatcherを生成するstaticメソッドです。
なお、matchingは内部でExampleMatcher#matchingAllで呼んでいます。

このサンプルは、nameが大文字／小文字を問わず"kitchen"で始まり、priceが800のデータを検索します。nameプロパティに対する検索条件はExampleMatcherで組み立てます。

```java
Item item = new Item();
item.setName("kitchen");
item.setPrice(800);

ExampleMatcher matcher = ExampleMatcher.matching()
    .withMatcher("name", match -> match.ignoreCase().startsWith());

Example<Item> example = Example.of(item, matcher);

List<Item> items = repository.findAll(example);
items.forEach(System.out::println);
```

#### matchingAny

検索条件で指定するプロパティのどれか1つ以上が一致するデータを検索するExampleMatcherを生成するstaticメソッドです。

このサンプルは、nameが大文字／小文字を問わず"kitchen"で始まるか、又はpriceが800のデータを検索します。

```java
Item item = new Item();
item.setName("kitchen");
item.setPrice(800);

ExampleMatcher matcher = ExampleMatcher.matchingAny()
    .withMatcher("name", match -> match.ignoreCase().startsWith());

Example<Item> example = Example.of(probe, matcher);

List<Item> items = repository.findAll(example);
items.forEach(System.out::println);
```

#### withIgnorePaths

検索に使用するエンティティのプロパティがnull以外だと検索条件として扱われてしまいます。特定のプロパティを検索条件としたくない場合はwithIgnorePathsでプロパティ名を指定して除外することができます。

このサンプルは、id, price, standardTypeプロパティを検索条件から除外します。

```java
ExampleMatcher matcher = ExampleMatcher.matchingAny()
    .withMatcher("name", match -> match.ignoreCase().startsWith())
    .withIgnorePaths("id", "price", "standardType");
```

#### withIncludeNullValues

nullの場合も検索したい場合はwithIncludeNullValuesを使用します。ただし、上記のwithIgnorePathsも併用して対象のプロパティを絞り込まないとすべてのプロパティに対して適用されてしまうので注意が必要です。SQL文では対応するカラムに"カラム名 is null"という条件が加えられます。

```java
ExampleMatcher matcher = ExampleMatcher.matchingAny()
    .withMatcher("name", match -> match.ignoreCase().startsWith())
    .withIncludeNullValues();
```