springboot×JPA×kotlinのリポジトリー周りを作り上げる

こんにちは!freegianの佐久間です。
弊社はAPI基盤に「SpringBoot×Kotlin」を採用し、Domain Driven Design(以下、DDD)で設計をしています。

前回は、以下のようなユースケースを作っていきました。

  • IDでデータ取得出来るようにする
  • 登録・更新出来るようにする
  • jpqlで検索する
  • sqlで検索する

今回は、DB前回積み残した以下のユースケースを完成させようと思います。

  • HibernateCriteriaでTypeSafeに検索してみる
  • 監査情報の自動更新

HibernateCriteriaでTypeSafeに検索してみる

普通にCriteriaApiを使うと、使いづらいので、ここではラップして利用します。

import java.util.*
import javax.persistence.criteria.CriteriaBuilder
import javax.persistence.criteria.Path
import javax.persistence.criteria.Predicate
import javax.persistence.criteria.Root

class RdbCriteriaCondition<T>(
  private val builder: CriteriaBuilder,
  private val root: Root<T>,
  private val predicates: MutableSet<Predicate> = mutableSetOf()
) {
  fun build(): Array<Predicate> {
    return this.predicates.toTypedArray()
  }
  private fun add(predicate: Predicate): RdbCriteriaCondition<T> {
    this.predicates.add(predicate)
    return this
  }
  fun equal(field: String, value: Any?): RdbCriteriaCondition<T> {
    if (isValid(value)) {
      val fieldPath: Path<Any> = root.get(field)
      add(builder.equal(fieldPath, value))
    }
    return this
  }
  private fun isValid(value: Any?): Boolean {
    return when (value) {
      is String? -> !value.isNullOrBlank()
      is Optional<*> -> value.isPresent
      else -> value != null
    }
  }
}
import javax.persistence.EntityManager
import javax.persistence.criteria.CriteriaBuilder
import javax.persistence.criteria.CriteriaQuery
import javax.persistence.criteria.Predicate
import javax.persistence.criteria.Root

class RdbCriteria<T>(
  private val entityClass: Class<T>,
  private val builder: CriteriaBuilder,
  private val query: CriteriaQuery<T>,
  private val root: Root<T>,
  private val condition: RdbCriteriaCondition<T>,
  private val predicates: MutableSet<Predicate> = mutableSetOf()
) {
  fun where(func: (RdbCriteriaCondition<T>) -> (RdbCriteriaCondition<T>)): RdbCriteria<T> {
    this.query.where(*func(condition).build())
    return this
  }
  fun result(): CriteriaQuery<T> {
    return result { it }
  }
  @Suppress("UNCHECKED_CAST")
  fun result(extension: (CriteriaQuery<T>) -> CriteriaQuery<T>): CriteriaQuery<T> {
    return extension(query)
  }
  companion object {
    private const val DefaultAlias = "r"
    fun <T> build(em: EntityManager, entityClass: Class<T>, alias: String = DefaultAlias): RdbCriteria<T> {
      val builder = em.criteriaBuilder
      val query = builder.createQuery(entityClass)
      val root = query.from(entityClass)
      root.alias(alias)
      return RdbCriteria(
        entityClass = entityClass,
        builder = builder,
        query = query,
        root = root,
        condition = RdbCriteriaCondition(root = root, builder = builder)
      )
    }
  }
}
@Repository
class RdbRepository (@PersistenceContext private val em: EntityManager) {
  fun <T> findByCriteria(entityClass: Class<T>, func: (RdbCriteria<T>) -> CriteriaQuery<T>): List<T> {
    return em.createQuery(func(RdbCriteria.build(em, entityClass))).resultList
  }
}

CriteriaApiラッパーは2つのクラスに分けています。
RdbCriteriaはCriteriaApi全体を管理するクラスで、RdbCriteriaConditionは条件を管理するクラスです。
分けている理由は、or条件やand条件を実現する際に、Condition管理するクラスがあった方が便利かなと考えたためです。
また、RdbCriteriaCondition#isValidで、パラメータのNullチェックをしています。
これのおかげで、動的に変わるパラメータを気にせずに条件式の構築が実現出来ます。
最後に、モデルから利用するためにRdbRepositoryにfindByCriteriaメソッドを生やせば完成です。

モデル側で利用する際は以下のようになります。

fun search(repository: RdbRepository, name: String): List<Shop> {
  return repository.findByCriteria(Shop::class.java) { criteria ->
    criteria.where { condition ->
      condition.equal("name", name)
    }.result()
  }
}

今回はサンプルなので、簡単な条件式のみ条件式のみ実装しています。
実運用では、以下のようなユースケースも必要かと思いますが、弊社では今回の思想に肉付けして利用しています。

  • or条件やand条件
  • like検索やin検索等
  • ページング
  • order条件
  • table join

監査情報の自動更新

@MappedSuperclass
abstract class BaseEntity<T: RdbEntity> : java.io.Serializable, RdbEntity {
  abstract var audit: RdbAuditModel?
  @PrePersist
  protected fun onCreate() {
    val now = LocalDateTime.now()
    val audit = RdbAuditModel(createdDate = now, updatedDate = now)
    this.audit = audit
  }
  @PreUpdate
  protected fun onUpdate() {
    val now = LocalDateTime.now()
    this.audit!!.updatedDate = now
  }
  @Embeddable
  class RdbAuditModel(
    /** 登録日時 */
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    var createdDate: LocalDateTime? = null,
    /** 更新日時 */
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    var updatedDate: LocalDateTime? = null
  ): java.io.Serializable
}

監査情報の自動更新を実現する為にはエンティティの親クラスを作成することで解決出来ます。
「@MappedSuperclass」を親クラスにつけて、登録時の処理を「@PrePersist」をつけたメソッドに、更新時の処理を「@PreUpdate」をつけたメソッドに記述すればおkです。
今回は、各Entityクラスにauditフィールドを定義させたかったので、abstractにしています。

モデル側で利用する際は以下のようになります。

@Entity
class Shop(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = -1,
    var name: String? = null,
    override var audit: RdbAuditModel? = null
): BaseEntity<Shop>()

まとめ

前回の記事に、HibernateCriteriaとEntitySuperClassを追加したのみですが、基本的なところは以上かなと思います。
ぜひ、ご自身で拡張してより良くしてみてください。基盤は生産性を握る非常に大事な部分ですからね!

今後の記事

ここまで、非常に簡単な基盤サンプルを作成してきましたが、実運用考えると全く使い物にならないと思います。
今後は、これまでのサンプルに以下のようなユースケースを追加していき、基盤として完成させていこうと思います。

  • Jacksonの循環参照対策
  • EntityのフィールドにEnumを適用する
  • 楽観的ロックで排他制御
  • HibernateでDDLを自動生成
  • Entityのみのテストで利用するDatafixtureを作る
  • 業務ロジックバリデーションのエラーとBeanバリデーションのエラーをシンプルに管理する
  • ステートレスなJsonWebTokenでセッション管理する
  • HttpClientを楽にする
  • AwsClientを楽にする

弊社では、基盤は出来上がってるんですが、全部出しちゃうとネタが無くなるのでご了承くださいwww

最後に

今回のサンプルは以下に公開しています。
https://github.com/freegian/kotlin-boot-jpa
本記事を見て、ご意見やご指摘等ございましたら、「[email protected]」まで是非ご連絡ください。

Twitterもやってます。ご興味ありましたら、フォローしてください。
Twitter:asakuma0401