JPA Auditing

JPA Auditingとは?

JPAでドメインをRDBMSのテーブルにマッピングすると、多くのドメインに共通するフィールドやカラムがあります。

  • 作成日時
  • 更新日時
  • 識別子

これらを各ドメインに定義すると重複コードが増えます。また、作成日時や更新日時は、データがいつ記録または変更されたかを残すため、保守において重要な情報です。

JPAにはこの情報を自動で設定するAuditing機能があります。永続性コンテキストへの保存や更新のたびに日時を設定する代わりに、Auditingを使用してデータベースのカラムへ自動的にマッピングできます。

プロジェクトの作成

data-jpalombokh2依存関係を含むSpring Bootプロジェクトを作成します。

curl https://start.spring.io/starter.tgz  \
-d bootVersion=2.5.0 \
-d dependencies=data-jpa,lombok,h2 \
-d baseDir=spring-jpa-auditing \
-d artifactId=jpa-auditing \
-d packageName=com.devkuma.jpa.auditing \
-d applicationName=Application \
-d packaging=jar \
-d javaVersion=1.8 \
-d type=gradle-project | tar -xzvf -

または、次のURLをブラウザーで開きます。

https://start.spring.io/#!type=gradle-project&language=java&platformVersion=2.5.0.RELEASE&packaging=jar&javaVersion=11&groupId=com.devkuma&artifactId=spring-jpa-auditing&name=spring-jpa-auditing&description=Demo%20project%20for%20Spring%20Boot&packageName=com.devkuma.jpa.auditing&dependencies=data-jpa,lombok,h2

ソースコードの実装

BaseTimeEntityクラス
domainパッケージにBaseTimeEntityを作成します。

package com.devkuma.jpa.auditing.domain;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}

BaseTimeEntityはすべてのEntityの親クラスであり、createdDatemodifiedDateを自動的に管理します。

  • @MappedSuperclass: 継承したフィールドをJPA Entityのカラムとして認識させます。
  • @EntityListeners(AuditingEntityListener.class): クラスにAuditing機能を提供します。
  • @CreatedDate: Entityの作成および保存時に日時を自動保存します。
  • @LastModifiedDate: Entityの更新時に日時を自動保存します。

Postクラス
EntityでBaseTimeEntityを継承します。

package com.devkuma.jpa.auditing.domain;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Getter
@NoArgsConstructor
@Entity
public class Post extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 500, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private String author;

    @Builder
    public Post(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
}
  • @Entity: テーブルに対応するクラスを指定します。
  • @Id: 主キーフィールドを指定します。

PostRepositoryクラス
JPA Repositoryを作成します。

package com.devkuma.jpa.auditing.repository;

import com.devkuma.jpa.auditing.domain.Post;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Post, String> {
}

Applicationクラス
mainクラスに@EnableJpaAuditingを追加します。

package com.devkuma.jpa.auditing;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}
  • @EnableJpaAuditing: JPA Auditingアノテーションを有効化します。

テスト

testソースフォルダーに次のテストを作成します。

package com.devkuma.jpa.auditing;

import com.devkuma.jpa.auditing.domain.Post;
import com.devkuma.jpa.auditing.repository.PostRepository;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@SpringBootTest
public class PostRepositoryTest {

    @Autowired
    PostRepository postRepository;

    @Test
    public void 게시글저장() {
        // given
        LocalDateTime now = LocalDateTime.of(2020, 8, 12, 0, 0, 0);
        postRepository.save(Post.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        // when
        List<Post> postsList = postRepository.findAll();

        //then
        Post posts = postsList.get(0);

        System.out.println(">>>>>>> createdDate=" + posts.getCreatedDate() + ", modifiedDate=" + posts.getModifiedDate());

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }
}

結果

... 省略 ...

2021-06-01 09:51:33.635  INFO 71886 --- [    Test worker] c.d.jpa.auditing.PostRepositoryTest      : Started PostRepositoryTest in 7.572 seconds (JVM running for 8.631)
>>>>>>> createdDate=2021-06-01T09:51:33.762582, modifiedDate=2021-06-01T09:51:33.762582
2021-06-01 09:51:34.006  INFO 71886 --- [extShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'

... 省略 ...

createdDatemodifiedDateに現在時刻が設定されていることを確認できます。

完成コード

完成したプロジェクトはこちらで確認できます。