What is it, naokirin?

Spring Data JPA (+ Kotlin)でレコードに作成日時、更新日時を設定できるようにする

本日は11月23日、新嘗祭の日ということで、ありがたくご飯を食べながら記事を書いていこうと思います。
(キンロウカンシャのひ?キンロウってなんですかね…? 🤔 )

今日は、Ruby on Railsでは当たり前のようにDBに生やす、 created_atupdated_at をSpring Data JPAでもやっちゃおうという記事です。

Spring Data JPA で作成日時、更新日時を設定する際の2つの問題

Spring Data JPAで作成日時、更新日時をEntityに追加する場合、当たり前ですが、そのままEntityに定義することもできます。

@Entity
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0,

    @field:NotBlank
    @field:Email
    var email: String = "",

    @field:NotBlank
    var createdAt: Date? = null

    @field:NotBlank
    var updatedAt: Date? = null
)

そして、各生成、更新の際に以下のように書いたりすることでもできます。

User(
    email = "example@example.com",
    createdAt = Date(),
    updatedAt = Date()
)

とはいえ、各実装ごとにいちいちこうした定義や実装はしたくないですし、それぞれの実装で日時を設定するタイミングが異なってしまったりする可能性もあり、極力共通化したいところです。

@MappedSuperclass でテーブル作成しない親クラスを定義する

基本的に、 作成日時、更新日時を定義する場合、すべてのEntityで定義されていてほしいと思います。

こうした場合、 @MappedSuperclass アノテーションをつけた abstract class に必要なカラムを定義して、各Entityで継承させることで共通のカラム定義を使い回すことができます

// 作成・更新日時のカラムを定義した親クラス
// @MappedSuperclassをつけているので、テーブル定義はされない
@MappedSuperclass
abstract class AbstractEntity {

    @field:NotBlank
    var createdAt: Date? = null

    @field:NotBlank
    var updatedAt: Date? = null
}

// 継承して、作成・更新日時のカラムが定義されるEntity
@Entity
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0,

    @field:NotBlank
    @field:Email
    var email: String = ""
) : AbstractEntity

継承になってしまうことで、機能ごとの定義をMixinのような形で使い回すことが難しくなってしまいますが、ほぼ例外なく定義したいカラムがある場合(例えば、今回の作成日時、更新日時や、auto incrementな id など)を共通化するのには非常に便利だと思います。

自動で作成日時、更新日時が設定されるようにする

ここまでで、カラム定義を共通化することができました。

ですが、このままでは作成日時、更新日時の設定は共通化できていません。

ここで使えるのが、各レコードへの処理に対するフックを定義できるアノテーションです。

@PrePersist
fun onPrePersist() {} // 新しいEntityが保存される前に呼ばれる
    
@PostPersist
fun onPostPersist() {} // 新しいEntityが保存された後に呼ばれる

@PreRemove
fun onPreRemove () {} // 削除前に呼ばれる
    
@PostRemove
fun onPostRemove() {} // 削除後に呼ばれる

@PreUpdate
fun onPreUpdate() {} // 更新前に呼ばれる

@PostUpdate
fun onPostUpdate() {} // 更新後に呼ばれる

@PreLoad
fun onPreLoad() {} // 読み込み前に呼ばれる

@PostLoad
fun onPostLoad() {} // 読み込み後に呼ばれる

今回は、作成時に日時を設定したいのと、更新時に日時を設定したいので、 @PrePersist@PreUpdate を使います。

@MappedSuperclass
abstract class AbstractEntity {
    // ... 省略 ...

    @PrePersist
    fun onPrePersist() {
        createdAt = Date()
        updatedAt = Date()
    }

    @PreUpdate
    fun onPreUpdate() {
        updatedAt = Date()
    }
}

これで、いちいち作成日時や更新日時を設定せずに、自動的に保存することができます。

まとめ

Spring Data JPAで共通化したい場合は、以下のようにすることで簡単に対応できます。

  • @MappedSuperclass をつけて定義したクラスに定義して、各Entityで継承することでカラム等の共通化が簡単になる
  • フック用のアノテーションをつけてメソッドを定義することで、レコードの変更や読み込み時の処理を共通化できる