エンティティを保存するたびに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_at と updated_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ステップで導入できます。
@EnableJpaAuditingを有効化するBaseEntityに@CreatedDate・@LastModifiedDateを定義する- エンティティで
extends BaseEntityする
Service層から日時セットのコードをすべて削除でき、設定漏れによるnullバグも防げます。AuditorAware を追加すれば操作ユーザーの記録まで一括管理できるので、ぜひ試してみてください。
JPA関連の記事として クエリメソッドの使い方 や パフォーマンス最適化 もあわせてどうぞ。