Specificationで検索APIを実装してみた

今回は自身で開発・運営している「Hello Books」で利用している技術について紹介したいと思います。  
  
#### [Hello Books](https://hellobooks.org)について
https://hellobooks.org

* エンジニア向け技術書レビューサービス
* 購入前の判断にエンジニアのレビューを役立てることができる
* アカウントを作成することで自身もレビューを投稿したり、技術書をブックマークしたりできる
* 詳細な条件による技術書の検索が可能
![スクリーンショット 2021-05-27 10.20.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/521599/52eb5728-a133-3759-011d-95571d1f006d.png)

### 検索機能の概要
[Hello Books](https://hellobooks.org)では書籍の検索機能を実装しています。キーワードだけでなく、カテゴリーやレーティング・価格などの複合的な条件に基づいて書籍の検索を行えます。
この検索機能の実装はSpringDataのSpecificationを利用して行っています。特に、単純な検索条件の実装はTomasz Kaczmarzykさんが開発した[specification-arg-resolver](https://github.com/tkaczmarzyk/specification-arg-resolver)をライブラリとして利用しています。それでは早速実装を紹介していきます。

### 検索用indexAPIの実装
書籍に関連するAPIは`BookController`に実装を行っています。検索リクエストはindexAPIで処理をしています。

```kotlin
@RestController
@RequestMapping("/api/book")
class BookController {

    @GetMapping("")
    fun index(
        @And(
            Spec(path = "category.value", params = ["category"], spec = Equal::class),
            Spec(path = "rating.value", params = ["minRating"], spec = GreaterThanOrEqual::class),
            Spec(path = "rating.value", params = ["maxRating"], spec = LessThanOrEqual::class),
            Spec(path = "price.value", params = ["minPrice"], spec = GreaterThanOrEqual::class),
            Spec(path = "price.value", params = ["maxPrice"], spec = LessThanOrEqual::class),
            Spec(path = "publishedAt.value", params = ["afterPublishedAt"], spec = GreaterThanOrEqual::class),
            Spec(path = "publishedAt.value", params = ["beforePublishedAt"], spec = LessThanOrEqual::class),
        )
        spec: Specification<Book>?,
        sort: Sort,
    ): ResponseEntity<Any> {
        val response = BookSearchRepository.search(spec, sort)
        return ResponseEntity.ok()
            .headers(response.returnHttpHeaders())
            .body(response.elements)
    }

}
```
引数に渡している`spec`が検索条件を含んだSpecificationインスタンスになります。[specification-arg-resolver](https://github.com/tkaczmarzyk/specification-arg-resolver)ライブラリのアノテーションを付けることで、非常に簡単に検索条件を設定できます。`@And`節で各種検索条件を設定していて、ここでは`path`・`param`・`spec`を設定するだけで自動的に検索条件に対応するSpecificationインスタンスを生成してくれています。

`path`が検索対象となるフィールドに対応していて、例えば`price.value`は書籍のpriceフィールドが`param`で設定しているよう`minPrice`・`maxPrice`としてリクエストに含まれる項目の検索対象となることを示しています。`.value`としているのはDDDを採用している関係でValue Objectの実質値を参照する必要があるためです。ある項目がprimitiveな型である場合には単純にプロパティ名だけを設定すればOKです。

`spec`節が具体的な検索条件に対応していて、`price`の例で言うと`GreaterThanOrEqual`・`LessThanOrEqual`で価格の範囲検索を行える様にしています。

ライブラリの詳細については公式[github](https://github.com/tkaczmarzyk/specification-arg-resolver)を参照してください。

### 検索に対応するRepositoryメソッドの実装
書籍のデータアクセス関連処理は`BookRepository`で実装しています。その中で、検索の処理は`search`メソッドで下記の様に実装しています。

```kotlin
@Repository
private interface BookSearchRepositoryBase
    : PagingAndSortingRepository<Book, Long>, JpaSpecificationExecutor<Book>

class BookSearchRepository {
    companion object {

        private fun repository(): BookSearchRepositoryBase =
            Resolver.resolve(BookSearchRepositoryBase::class.java)

        fun search(
            spec: Specification<Book>?,
            sort: Sort
        ): PagingResponse<Book> {
            val entities = repository().findAll(spec, sort)
            return PagingResponse(
                entities.size.toLong(),
                0L,
                0L,
                0L,
                0L,
                entities
            )
        }
    }
```

Specificationでの検索を実行するため`PagingAndSortingRepository<Book, Long>, JpaSpecificationExecutor<Book>`を継承したinterfaceを`repository`メソッドでインスタンス化することでRepositoryのメソッドをstatic的に呼び出せる様にしています（Specification検索実装のトピックから外れてしまうため、詳細は割愛します）。

Controllerで[specification-arg-resolver](https://github.com/tkaczmarzyk/specification-arg-resolver)を利用して設定した`spec`とソーティングを設定した`sort`を`findAll`メソッドに渡すことで、簡単に検索結果を取得できます！

最後に`entities`を`PagingResponse`として返却することで、検索結果を含めたjsonをHttpResponseとして設定しています。`0L`として設定しているのはページングの設定箇所になります。今回は説明の簡略化のためページングの実装は省いて解説しているため、全て`0L`として設定しています（もし"ページングについても知りたい！"という方がいらっしゃいましたらコメント欄にてお知らせください）。

##### 最後に
最後まで読んでくださり、ありがとうございました。[specification-arg-resolver](https://github.com/tkaczmarzyk/specification-arg-resolver)では満たせない検索機能などは"resolverで生成されたSpecificationに独自実装で設定したSpecificationをマージする"などの対応が必要になることもありますが、基本的な検索機能であれば簡単に実装できることがお分かりいただけたと思います。よろしければ[Hello Books](https://hellobooks.org)にも是非遊びに来てください！