今回は自身で開発・運営している「Hello Books」で利用している技術について紹介したいと思います。
Hello Booksについて
- エンジニア向け技術書レビューサービス
- 購入前の判断にエンジニアのレビューを役立てることができる
- アカウントを作成することで自身もレビューを投稿したり、技術書をブックマークしたりできる
- 詳細な条件による技術書の検索が可能
検索機能の概要
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
節で各種検索条件を設定していて、ここではpath
・param
・spec
を設定するだけで自動的に検索条件に対応するSpecificationインスタンスを生成してくれています。
path
が検索対象となるフィールドに対応していて、例えばprice.value
は書籍のpriceフィールドがparam
で設定しているようminPrice
・maxPrice
としてリクエストに含まれる項目の検索対象となることを示しています。.value
としているのはDDDを採用している関係でValue Objectの実質値を参照する必要があるためです。ある項目がprimitiveな型である場合には単純にプロパティ名だけを設定すればOKです。
spec
節が具体的な検索条件に対応していて、price
の例で言うとGreaterThanOrEqual
・LessThanOrEqual
で価格の範囲検索を行える様にしています。
ライブラリの詳細については公式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
とソーティングを設定したsort
をfindAll
メソッドに渡すことで、簡単に検索結果を取得できます!
最後にentities
をPagingResponse
として返却することで、検索結果を含めたjsonをHttpResponseとして設定しています。0L
として設定しているのはページングの設定箇所になります。今回は説明の簡略化のためページングの実装は省いて解説しているため、全て0L
として設定しています(もし"ページングについても知りたい!"という方がいらっしゃいましたらコメント欄にてお知らせください)。
最後に
最後まで読んでくださり、ありがとうございました。specification-arg-resolverでは満たせない検索機能などは"resolverで生成されたSpecificationに独自実装で設定したSpecificationをマージする"などの対応が必要になることもありますが、基本的な検索機能であれば簡単に実装できることがお分かりいただけたと思います。よろしければHello Booksにも是非遊びに来てください!