JPAでジェネリクスリポジトリーを作ってみた

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

弊社はJPAを利用しているのですが、既存のJpaRepositoryがあまりにイケてなかった(コントリビュートしろ)ので、
entityManagerをラップしてGenericsRepositoryを作成しました。
今日はその知見を共有します。

今回は、以下のようなユースケースが出来上がったらゴールです。

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

基本的にはバインド変数が使えるjpqlを利用し、複雑なクエリー等にはsqlを利用すると考えてます。

まずはRepository作成

class RdbRepository (@PersistenceContext private val em: EntityManager) {}

リポジトリー完成しました。笑
ちゃんと説明すると、今回はEntityManagerを直接利用することにしました。
@PersistenceContextで、Configを設定しておけばSpringBootが勝手に作ってくれるEntityManagerを呼び出せます。
後は、ここにメソッドを追加していきます。

続いて、IDでデータ取得出来るようにする

class RdbRepository (@PersistenceContext private val em: EntityManager) {
  fun <T : RdbEntity> get(clazz: Class<T>, id: Serializable ) : T? {
    return em.find(clazz, id)
  }
}

EntityManagerにはfindというメソッドがあるので、それを利用します。
EntityManagerのfindメソッドは、エンティティがなかった場合、nullを返してくるのでそこだけ注意です。
エンティティクラスにはRdbEntityというマーカーインターフェイスをつけており、エンティティクラス以外は弾くようになっています。

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

@Entity
class Shop(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long? = -1,
  var name: String? = null
): RdbEntity {
  companion object {
    fun get(repository: RdbRepository, id: Long): Shop? {
      return repository.get(Shop::class.java, id)
    }
  }
}

以外と簡単ですよね?

登録・更新・削除出来るようにする

class RdbRepository (@PersistenceContext private val em: EntityManager) {
  fun <T : RdbEntity> save(entity: T): T {
    em.persist(entity)
    return entity
  }
  fun <T : RdbEntity> update(entity: T): T {
    return em.merge(entity)
  }
  fun <T : RdbEntity> delete(entity: T): T {
    em.remove(entity)
    return entity
  }
}

登録には「persist」を、更新には「merge」を、削除には「remove」メソッドを利用します。
利用方法はエンティティクラスを渡すだけなので考えることは少ないです。

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

@Entity
class Shop(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long? = -1,
  var name: String? = null
): RdbEntity {
  companion object {
    fun register(repository: RdbRepository, name: String): Shop {
      return repository.save(Shop(name = name))
    }
  }
  fun update(repository: RdbRepository, name: String): Shop {
    this.name = name
    return repository.update(this)
  }
  fun delete(repository: RdbRepository): Shop {
    return repository.delete(this)
  }
}

jpqlで検索する

class RdbRepository (@PersistenceContext private val em: EntityManager) {
  fun <T : RdbEntity> get(jpql: String, vararg args: Any) : T? {
    return bindQueryParams(em.createQuery(jpql), args).resultList.firstOrNull() as T?
  }
  private fun bindQueryParams(query: Query, vararg args: Any): Query {
    for (i in args.indices) {
      query.setParameter(i + 1, args[i])
    }
    return query
  }
}

jpqlをcreateQueryメソッドでQueryを作成し、resultListで結果を取得出来ます。
動的なパラメータはcreateQueryメソッドで作成されたQueryクラスのsetParameterメソッドで動的にパラメータを追加出来ます。
今回はbindQueryParamsメソッドで位置パラメータをQueryにバインドしています。

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

@Entity
class Shop(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long? = -1,
  var name: String? = null
): RdbEntity {
  companion object {
    fun get(repository: RdbRepository, name: String): Shop? {
      return repository.get("from Shop where name != ?1", name)
    }
  }
}

sqlで検索する

class RdbRepository (@PersistenceContext private val em: EntityManager) {
  fun <T : RdbEntity> get(jpql: String, vararg args: Any) : T? {
    return bindQueryParams(em.createNativeQuery(sql), *args).resultList.firstOrNull() as T?
  }
  private fun bindQueryParams(query: Query, vararg args: Any): Query {
    for (i in args.indices) {
      query.setParameter(i + 1, args[i])
    }
    return query
  }
}

jpqlの場合はcreateQueryを利用しましたが、sqlで検索する場合はcreateNativeQueryになります。
差分はそれのみです

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

@Entity
class Shop(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long? = -1,
  var name: String? = null
): RdbEntity {
  companion object {
    fun get(repository: RdbRepository, name: String): Shop? {
      return repository.get("select * from shop where name != ?1", name)
    }
  }
}

まとめ

RdbRepositoryを作成しておくことで、EntityごとにRepositoryを作成するという苦しみから解放されました。
また、エンティティクラスにQueryが出されることによって、業務ロジックの集約が達成できました。
弊社では今回ご紹介したRdbRepositoryを拡充し、利用しています。

次回予告!!!

次回は以下のようなコンテンツをご紹介します!

  • criteriaで検索する
  • エンティティクラスの更新日や登録日の自動更新

最後に

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