Menu

キーワード

  • JPA
  • Spring Boot
  • Kotlin

目次

したいこと

ひとまず最小でありがちな関連と永続化処理を実装したい。

エンティティ(単体)

まずはただIDで識別できる単独の実体。

モデル

@Entity
@Table
class User(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  var name: String = "",

  @Version
  var version: Long = -1
)

エンティティにはIDが必要

JPAのエンティティはID(プライマリキー)を持つ、のが基本。

@Versionを設定

エンティティを楽観ロックするならバージョンを設ける。

永続化処理

User(name = "山田太郎")
  .also { entityManager.persist(it) }

Kotlinで実装する場合デフォルト値の定義が必要

Kotlinでエンティティを実装する場合、デフォルト値を定義しておく必要がある。デフォルト値を定義しておかないと、エンティティ永続化時にエラーが発生する。永続化時に値があるかどうかではないことに注意。エラーメッセージからだと原因がなんとも想像つかない。

detached entity passed to persist spring jpa

伝票・明細、集約等(1対多)の関係

伝票・明細の関係。伝票単位で明細も含めて一式保存するものとして。

モデル

Slipを伝票、Detailを明細として。

@Entity
@Table
class Slip(
  @OneToMany(mappedBy = "slip", cascade = [CascadeType.ALL], orphanRemoval = true)
  var details: MutableList<Detail> = emptyList<Detail>().toMutableList()

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  var name: String = "",

  @Version
  var version: Long = -1
)
@Entity
@Table
class Detail(
  @ManyToOne
  @JoinColumn(name = "slip_id", referencedColumnName = "id")
  var slip: Slip? = null,

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  var name: String = ""
)

@OneToManyの設定

mappedBy
伝票側エンティティが明細側エンティティから何として関連しているか
cascade
明細側エンティティにカスケードする永続化オペレーション
orphanRemoval
リレーションから削除した明細側エンティティに削除操作を適用する

@ManyToOne (@JoinColumn)の設定

name
伝票側エンティティを指定する明細側エンティティ上の外部キー
referencedColumnName
その外部キーに該当する伝票側エンティティの主キー

伝票側に@Versionを設定

伝票側エンティティに楽観ロック用のバージョンを設ける。

永続化処理

Slip(name = "伝票")
  .apply { detils.add(Detail(slip = this, name = "明細")) }
  .also { entityManager.persist(it) }

明細側エンティティの永続化処理は不要

永続化オペレーションをカスケードするのでpersistするのは伝票側エンティティだけでいい。

イベントやログ、ジョブ等(多対1)の関係

ある実体に紐づくイベントやログ、ジョブ等の関係。親側エンティティの単位で子側エンティティ(イベントやログ、ジョブ等)を保存しないものとして。

モデル

Dealを取引、DealEventを取引イベントとして。

@Entity
@Table
class Deal(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  @Version
  var version: Long = -1
)
@Entity
@Table
class DealEvent(
  @ManyToOne
  @JoinColumn(name = "foo_id", referencedColumnName = "id")
  var deal: Deal? = null,

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  var name: String = "",

  var raisedAt: LocalDateTime = LocalDateTime.now()
)

@ManyToOne (@JoinColumn)の設定

伝票・明細、集約等(1対多)の関係、明細側のエンティティと同じ。

name
親側エンティティを指定する子側エンティティ上の外部キー
referencedColumnName
その外部キーに該当する伝票側エンティティの主キー

永続化処理

entityManager.find(Deal::class.java, dealId)
  ?.let { DealEvent(deal = it, name = "取引イベント") }
  ?.also { entityManager.persist(it) }

managedな親側エンティティが必要

子側エンティティにセットする親側エンティティはmanagedである必要がある(はず)。

カーソルとスナップショット等(1対1)の関係

ある実体を指すカーソルとしての関係。カーソルはすでに存在するスナップショット等を指すものとして。

モデル

@Entity
@Table
class Cursor(
  @OneToOne
  @JoinColumn(name = "details_id", referencedColumnName = "id")
  var details: Details? = null,

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  @Version
  var version: Long = -1
)
@Entity
@Table
class Details(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  var name: String = "",
 
  @Version
  var version: Long = -1   
)

@OneToOne (@JoinColumn)の設定

イベントやログ、ジョブ等(多対1)の関係、子側のエンティティと同じ。

name
ターゲット側エンティティを指定するカーソル側エンティティ上の外部キー
referencedColumnName
その外部キーに該当するターゲット側エンティティの主キー

永続化処理

Details(name = "スナップショット等")
  .also { entityManager.persist(it) }
  .let { Cursor(details = it) }
  .also { entityManager.persist(it) }

カーソル側エンティティは子として処理する

ターゲット側のエンティティはmanagedである必要があり、カーソル側のエンティティは子として処理する。

任意の関連をもつ(1対0,1)関係

ユーザーについて固定の記事など、任意の関連をもつ関係。関連は関連テーブルにして、存在しない場合も許すものとして。

モデル

@Entity
@Table
class User(
  @OneToOne
  @JoinTable(
    name = "user_fixed_post",
    joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
    inverseJoinColumns = [JoinColumn(name = "post_id", referencedColumnName = "id")]
  )
  var fixedPost: Post? = null,

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  var name: String = "",

  @Version
  var version: Long = -1
)
@Entity
@Table
class Post(
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  var id: Long = -1,

  var name: String = "",

  @Version
  var version: Long = -1
)

@OneToOne (@JoinTable, JoinColumn)の設定

joinColumns
ユーザー側エンティティを指定する関連テーブルの外部キー(name)と、ユーザー側エンティティの主キー(referencedColumnName)
inverseJoinColumns
記事側エンティティを指定する関連テーブルの外部キー(name)と、記事側エンティティの主キー(referencedColumnName)

永続化処理

User(name = "ユーザー")
  .apply { fixedPost = entityManager.find(Post::class.java, postId) }
  .also { entityManager.persist(it) }

記事側エンティティは子のようであるがmanaged

伝票・明細、集約等(1対多)の関係のように、記事側エンティティをフィールドにセットしてユーザー側エンティティを保存する。このとき、記事側エンティティはmanagedである必要がある(はず)。

参考