エンティティを保存するたびにService層で entity.setCreatedAt(LocalDateTime.now()) を書いていませんか。設定し忘れるとDBのカラムがnullになり、後から気づきにくいバグになります。

Spring Data JPAには JPA Auditing という機能があり、アノテーションを付けるだけで作成日時・更新日時を自動的に記録できます。Service層のコードを一切変更せずに済むうえ、作成者・更新者も同じ仕組みで記録できます。

JPA Auditingを有効化する

まず依存関係を確認します。spring-boot-starter-data-jpa があれば追加の依存は不要です。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}

次に @EnableJpaAuditing を有効化します。最もシンプルな方法はメインクラスへの追加ですが、テスト時の問題を避けるため専用の @Configuration クラスに切り出す方が実務ではおすすめです。

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}

これだけでAuditing機能がオンになります。忘れがちな設定なので、まず最初に確認しましょう。

BaseEntityを作成する

作成日時・更新日時フィールドを共通化するために BaseEntity クラスを用意します。

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedAt;

    // getter省略
}

ポイントを3つ押さえておきます。

  • @MappedSuperclass を付けることで、このクラス自体はテーブルにならず、フィールドだけが継承先に引き継がれます
  • @EntityListeners(AuditingEntityListener.class)必須 です。これがないとAuditingが動きません
  • updatable = false を付けると createdAt が更新時に上書きされなくなります

ZonedDateTime を使いたい場合はDBのカラム型が TIMESTAMP WITH TIME ZONE に対応しているか確認してください。アプリがシングルタイムゾーンなら LocalDateTime で十分です。

エンティティで継承する

あとは extends BaseEntity するだけです。

@Entity
@Table(name = "articles")
public class Article extends BaseEntity {

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

    private String title;
    private String content;
}

この1行を追加するだけで created_atupdated_at が自動記録されるようになります。既存エンティティへの適用も同様です。エンティティのリレーションマッピング を参照しながら設計している場合も、継承を加えるだけで済みます。

動作確認

@DataJpaTest を使った軽量なテストで確認できます。

@DataJpaTest
@Import(JpaAuditingConfig.class)  // Auditingを有効化するために必要
class ArticleRepositoryTest {

    @Autowired
    private ArticleRepository articleRepository;

    @Test
    void 保存時にcreatedAtとupdatedAtが自動セットされる() {
        Article saved = articleRepository.save(new Article("テスト記事", "本文"));

        assertThat(saved.getCreatedAt()).isNotNull();
        assertThat(saved.getUpdatedAt()).isNotNull();
    }

    @Test
    void 更新時にupdatedAtだけが変わる() {
        Article article = articleRepository.save(new Article("元タイトル", "本文"));
        LocalDateTime createdAt = article.getCreatedAt();

        article.setTitle("新タイトル");
        Article updated = articleRepository.saveAndFlush(article);

        assertThat(updated.getCreatedAt()).isEqualTo(createdAt);
        assertThat(updated.getUpdatedAt()).isAfter(createdAt);
    }
}

@DataJpaTest では @Configuration クラスが読み込まれないため、@Import(JpaAuditingConfig.class) が必要です。これを忘れると日時がnullになるので注意してください。

@CreatedBy・@LastModifiedByで操作ユーザーも記録する

操作したユーザーも記録したい場合は AuditorAware インターフェースを実装します。まず BaseEntity に2フィールドを追加します。

@CreatedBy
@Column(updatable = false)
private String createdBy;

@LastModifiedBy
private String updatedBy;

次に AuditorAware を実装してBean登録します。Spring Securityを使っている場合は SecurityContextHolder からユーザー名を取得できます。

@Component("auditorProvider")
public class SpringSecurityAuditorAware implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.ofNullable(
            SecurityContextHolder.getContext().getAuthentication()
        )
        .filter(auth -> auth.isAuthenticated()
            && !(auth instanceof AnonymousAuthenticationToken))
        .map(Authentication::getName);
    }
}

Spring Securityを使わない場合は Optional.of("system") のように固定値を返す実装でも問題ありません。最後に @EnableJpaAuditing 側でBean名を指定します。

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaAuditingConfig {
}

Spring Security自体の認証設定については JWT認証の記事 を参照してください。

ハマりやすいポイント

日時がnullになる場合

@EnableJpaAuditing の設定漏れか、@EntityListeners(AuditingEntityListener.class) の指定漏れがほとんどです。@DataJpaTest で動かないときは @Import も確認しましょう。

updated_atが変わらない場合

トランザクション外で操作している可能性があります。トランザクション管理の記事 も合わせて確認してください。

ZonedDateTimeでエラーになる場合

MySQLなどでは TIMESTAMP WITH TIME ZONE 相当のカラム型が必要です。対応が難しい場合は LocalDateTime への変更も選択肢です。

まとめ

JPA Auditingは次の3ステップで導入できます。

  1. @EnableJpaAuditing を有効化する
  2. BaseEntity@CreatedDate@LastModifiedDate を定義する
  3. エンティティで extends BaseEntity する

Service層から日時セットのコードをすべて削除でき、設定漏れによるnullバグも防げます。AuditorAware を追加すれば操作ユーザーの記録まで一括管理できるので、ぜひ試してみてください。

JPA関連の記事として クエリメソッドの使い方パフォーマンス最適化 もあわせてどうぞ。