LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-06-25

今回は自身で開発・運営している「Hello Books」で利用している技術について紹介したいと思います。

Hello Booksについて

  • エンジニア向け技術書レビューサービス
  • 購入前の判断にエンジニアのレビューを役立てることができる
  • アカウントを作成することで自身もレビューを投稿したり、技術書をブックマークしたりできる
  • 詳細な条件による技術書の検索が可能 スクリーンショット 2021-05-27 10.20.00.png

検索機能の概要

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

検索用indexAPIの実装

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

@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ライブラリのアノテーションを付けることで、非常に簡単に検索条件を設定できます。@And節で各種検索条件を設定していて、ここではpathparamspecを設定するだけで自動的に検索条件に対応するSpecificationインスタンスを生成してくれています。

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

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

ライブラリの詳細については公式githubを参照してください。

検索に対応するRepositoryメソッドの実装

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

@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を利用して設定したspecとソーティングを設定したsortfindAllメソッドに渡すことで、簡単に検索結果を取得できます!

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

最後に

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0