Spring Data JPA Auditingは、エンティティの作成者・更新者・作成日時・更新日時を自動的に記録する監査機能です。データベースに「誰が」「いつ」レコードを作成・変更したかを追跡することは、セキュリティ監査やデータの信頼性確保において重要な要件となります。本記事では、@EnableJpaAuditingの設定からAuditorAwareによるSpring Security連携、@MappedSuperclassを使った監査カラムの共通化まで、Spring Data JPA Auditingの実装方法を体系的に解説します。

実行環境と前提条件

項目 バージョン
Java 17以上(推奨: 21)
Spring Boot 3.4.x
Spring Data JPA 3.4.x
Hibernate 6.6.x
データベース PostgreSQL 16 / H2(テスト用)

前提条件として、以下の依存関係を含むSpring Bootプロジェクトが作成済みであることを想定しています。

1
2
3
4
5
6
7
// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation 'com.h2database:h2'
}

Spring Data JPA Auditingの概要

Spring Data JPA Auditingは、エンティティのライフサイクルイベント(保存・更新)に対して、監査情報を自動的に付与する仕組みです。

flowchart LR
    subgraph App["アプリケーション"]
        A[Repository.save]
    end
    
    subgraph Audit["Auditing機構"]
        B[AuditingEntityListener]
        C[AuditorAware]
        D[DateTimeProvider]
    end
    
    subgraph Entity["エンティティ"]
        E["@CreatedBy<br/>@LastModifiedBy"]
        F["@CreatedDate<br/>@LastModifiedDate"]
    end
    
    A --> B
    B --> C
    B --> D
    C --> E
    D --> F

主要なコンポーネントの役割は以下のとおりです。

コンポーネント 役割
@EnableJpaAuditing Auditing機能を有効化する設定アノテーション
AuditingEntityListener エンティティの保存・更新イベントを監視し、監査情報を設定
AuditorAware<T> 現在のユーザー(操作者)を取得するためのインターフェース
DateTimeProvider 現在日時を取得するためのインターフェース(デフォルト: システム時刻)

@EnableJpaAuditingの設定

Auditing機能を有効化するには、@EnableJpaAuditingアノテーションを設定クラスに付与します。

1
2
3
4
5
6
7
8
9
package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}

@EnableJpaAuditingには以下の属性を設定できます。

属性 デフォルト値 説明
auditorAwareRef (自動検出) AuditorAware Beanの名前を明示的に指定
dateTimeProviderRef (自動検出) DateTimeProvider Beanの名前を指定
modifyOnCreate true 作成時に@LastModifiedBy/@LastModifiedDateも設定するか
setDates true 日時フィールドを設定するか

複数のAuditorAware実装が存在する場合は、auditorAwareRefで明示的に指定します。

1
2
3
4
@Configuration
@EnableJpaAuditing(auditorAwareRef = "securityAuditorAware")
public class JpaAuditingConfig {
}

監査アノテーションの使い方

Spring Data JPAは4つの監査アノテーションを提供しています。

@CreatedDateと@LastModifiedDate

@CreatedDateはエンティティの作成日時、@LastModifiedDateは最終更新日時を自動設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.example.entity;

import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;

@Entity
@Table(name = "articles")
@EntityListeners(AuditingEntityListener.class)
public class Article {

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

    @Column(nullable = false)
    private String title;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private Instant updatedAt;

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Instant getCreatedAt() {
        return createdAt;
    }

    public Instant getUpdatedAt() {
        return updatedAt;
    }
}

日時フィールドで使用できる型は以下のとおりです。

説明
java.time.Instant UTC基準のタイムスタンプ(推奨)
java.time.LocalDateTime タイムゾーンなしの日時
java.time.ZonedDateTime タイムゾーン付き日時
java.time.OffsetDateTime オフセット付き日時
java.util.Date 旧API(非推奨)
java.util.Calendar 旧API(非推奨)
long / Long エポックミリ秒

@CreatedByと@LastModifiedBy

@CreatedByはエンティティの作成者、@LastModifiedByは最終更新者を自動設定します。これらのアノテーションを使用するには、AuditorAwareインターフェースの実装が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.example.entity;

import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;

@Entity
@Table(name = "documents")
@EntityListeners(AuditingEntityListener.class)
public class Document {

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

    @Column(nullable = false)
    private String name;

    @CreatedBy
    @Column(name = "created_by", nullable = false, updatable = false)
    private String createdBy;

    @LastModifiedBy
    @Column(name = "updated_by", nullable = false)
    private String updatedBy;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private Instant updatedAt;

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCreatedBy() {
        return createdBy;
    }

    public String getUpdatedBy() {
        return updatedBy;
    }

    public Instant getCreatedAt() {
        return createdAt;
    }

    public Instant getUpdatedAt() {
        return updatedAt;
    }
}

AuditorAwareインターフェースの実装

AuditorAware<T>は、現在の操作者(Auditor)を取得するためのSPIインターフェースです。@CreatedByおよび@LastModifiedByを使用する場合は、このインターフェースの実装が必須となります。

Spring Security連携の実装

Spring Securityを使用している場合は、SecurityContextHolderから認証情報を取得します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.config;

import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component("securityAuditorAware")
public class SecurityAuditorAware implements AuditorAware<String> {

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

認証されていない場合やシステム処理でデフォルト値を設定したい場合は、以下のように実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.config;

import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component("securityAuditorAware")
public class SecurityAuditorAware implements AuditorAware<String> {

    private static final String SYSTEM_USER = "SYSTEM";

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.ofNullable(SecurityContextHolder.getContext())
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .filter(auth -> !auth.getName().equals("anonymousUser"))
                .map(Authentication::getName)
                .or(() -> Optional.of(SYSTEM_USER));
    }
}

UserDetailsからカスタム情報を取得

ユーザーIDなど、ユーザー名以外の情報を監査者として記録したい場合は、UserDetails実装クラスから取得します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.config;

import com.example.security.CustomUserDetails;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class UserIdAuditorAware implements AuditorAware<Long> {

    @Override
    public Optional<Long> getCurrentAuditor() {
        return Optional.ofNullable(SecurityContextHolder.getContext())
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getPrincipal)
                .filter(principal -> principal instanceof CustomUserDetails)
                .map(principal -> ((CustomUserDetails) principal).getUserId());
    }
}

対応するエンティティでは、@CreatedBy@LastModifiedByの型をLongに変更します。

1
2
3
4
5
6
7
@CreatedBy
@Column(name = "created_by_user_id", nullable = false, updatable = false)
private Long createdByUserId;

@LastModifiedBy
@Column(name = "updated_by_user_id", nullable = false)
private Long updatedByUserId;

HTTPヘッダーからユーザー情報を取得

APIゲートウェイ経由でユーザー情報がHTTPヘッダーに設定される場合は、RequestContextHolderを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.config;

import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Optional;

@Component
public class HeaderBasedAuditorAware implements AuditorAware<String> {

    private static final String USER_ID_HEADER = "X-User-Id";
    private static final String DEFAULT_USER = "SYSTEM";

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
                .filter(attrs -> attrs instanceof ServletRequestAttributes)
                .map(attrs -> (ServletRequestAttributes) attrs)
                .map(ServletRequestAttributes::getRequest)
                .map(request -> request.getHeader(USER_ID_HEADER))
                .filter(userId -> !userId.isBlank())
                .or(() -> Optional.of(DEFAULT_USER));
    }
}

@MappedSuperclassによる監査カラム共通化

監査カラムは多くのエンティティで共通して必要となるため、@MappedSuperclassを使用して基底クラスに共通化することが推奨されます。

基底エンティティクラスの定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.example.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;

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

    @CreatedBy
    @Column(name = "created_by", nullable = false, updatable = false, length = 100)
    private String createdBy;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedBy
    @Column(name = "updated_by", nullable = false, length = 100)
    private String updatedBy;

    @LastModifiedDate
    @Column(name = "updated_at", nullable = false)
    private Instant updatedAt;

    public String getCreatedBy() {
        return createdBy;
    }

    public Instant getCreatedAt() {
        return createdAt;
    }

    public String getUpdatedBy() {
        return updatedBy;
    }

    public Instant getUpdatedAt() {
        return updatedAt;
    }

    // 内部利用・テスト用のSetter(通常は使用しない)
    protected void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    protected void setCreatedAt(Instant createdAt) {
        this.createdAt = createdAt;
    }

    protected void setUpdatedBy(String updatedBy) {
        this.updatedBy = updatedBy;
    }

    protected void setUpdatedAt(Instant updatedAt) {
        this.updatedAt = updatedAt;
    }
}

@MappedSuperclassを使用する際のポイントは以下のとおりです。

ポイント 説明
テーブル未生成 @MappedSuperclassが付与されたクラス自体はテーブルを生成しない
フィールド継承 定義したフィールドは継承先エンティティのカラムとしてマッピングされる
@EntityListenersの継承 基底クラスに付与した@EntityListenersは継承先に自動適用される
updatable = false 作成時の情報は更新されないよう明示的に指定する

継承先エンティティの定義

基底クラスを継承したエンティティを定義します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.example.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "orders")
public class Order extends BaseAuditableEntity {

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

    @Column(name = "order_number", nullable = false, unique = true, length = 50)
    private String orderNumber;

    @Column(name = "total_amount", nullable = false)
    private Integer totalAmount;

    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false, length = 20)
    private OrderStatus status;

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getOrderNumber() {
        return orderNumber;
    }

    public void setOrderNumber(String orderNumber) {
        this.orderNumber = orderNumber;
    }

    public Integer getTotalAmount() {
        return totalAmount;
    }

    public void setTotalAmount(Integer totalAmount) {
        this.totalAmount = totalAmount;
    }

    public OrderStatus getStatus() {
        return status;
    }

    public void setStatus(OrderStatus status) {
        this.status = status;
    }
}
1
2
3
4
5
6
7
8
9
package com.example.entity;

public enum OrderStatus {
    PENDING,
    CONFIRMED,
    SHIPPED,
    DELIVERED,
    CANCELLED
}

生成されるテーブル構造は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
CREATE TABLE orders (
    id              BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    order_number    VARCHAR(50) NOT NULL UNIQUE,
    total_amount    INTEGER NOT NULL,
    status          VARCHAR(20) NOT NULL,
    created_by      VARCHAR(100) NOT NULL,
    created_at      TIMESTAMP WITH TIME ZONE NOT NULL,
    updated_by      VARCHAR(100) NOT NULL,
    updated_at      TIMESTAMP WITH TIME ZONE NOT NULL
);

@EntityListenersとAuditingEntityListenerの役割

@EntityListenersは、JPAエンティティのライフサイクルイベントをリッスンするクラスを指定するアノテーションです。AuditingEntityListenerは、Spring Data JPAが提供するリスナー実装で、監査情報の自動設定を担当します。

AuditingEntityListenerの内部動作

sequenceDiagram
    participant App as アプリケーション
    participant EM as EntityManager
    participant Listener as AuditingEntityListener
    participant Handler as AuditingHandler
    participant Auditor as AuditorAware
    participant DateTime as DateTimeProvider

    App->>EM: persist(entity)
    EM->>Listener: @PrePersist
    Listener->>Handler: markCreated(entity)
    Handler->>Auditor: getCurrentAuditor()
    Auditor-->>Handler: Optional<User>
    Handler->>DateTime: getNow()
    DateTime-->>Handler: Optional<TemporalAccessor>
    Handler->>Handler: Set @CreatedBy, @CreatedDate
    Handler->>Handler: Set @LastModifiedBy, @LastModifiedDate
    
    App->>EM: merge(entity)
    EM->>Listener: @PreUpdate
    Listener->>Handler: markModified(entity)
    Handler->>Auditor: getCurrentAuditor()
    Auditor-->>Handler: Optional<User>
    Handler->>DateTime: getNow()
    DateTime-->>Handler: Optional<TemporalAccessor>
    Handler->>Handler: Set @LastModifiedBy, @LastModifiedDate

AuditingEntityListenerは以下の2つのコールバックメソッドを持ちます。

コールバック 発火タイミング 設定される監査情報
@PrePersist エンティティの新規保存前 @CreatedBy, @CreatedDate, @LastModifiedBy, @LastModifiedDate
@PreUpdate エンティティの更新前 @LastModifiedBy, @LastModifiedDate

グローバル設定による自動適用

@EntityListenersを各エンティティに付与する代わりに、orm.xmlでグローバルに設定することもできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm
                 https://jakarta.ee/xml/ns/persistence/orm/orm_4_0.xsd"
                 version="3.2">
    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener"/>
            </entity-listeners>
        </persistence-unit-defaults>
    </persistence-unit-metadata>
</entity-mappings>

この設定ファイルをsrc/main/resources/META-INF/orm.xmlに配置すると、すべてのエンティティにAuditingEntityListenerが自動適用されます。ただし、明示的に@EntityListenersを基底クラスに付与する方法の方が、設定の把握が容易であるため推奨されます。

カスタムDateTimeProviderの実装

テスト時に日時を固定したい場合や、特定のタイムゾーンを使用したい場合は、DateTimeProviderインターフェースを実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.example.config;

import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.stereotype.Component;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;

@Component("customDateTimeProvider")
public class CustomDateTimeProvider implements DateTimeProvider {

    private static final ZoneId ZONE_ID = ZoneId.of("Asia/Tokyo");

    @Override
    public Optional<TemporalAccessor> getNow() {
        return Optional.of(ZonedDateTime.now(ZONE_ID));
    }
}

設定クラスでDateTimeProviderを明示的に指定します。

1
2
3
4
@Configuration
@EnableJpaAuditing(dateTimeProviderRef = "customDateTimeProvider")
public class JpaAuditingConfig {
}

テスト用の固定日時Provider

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.config;

import org.springframework.data.auditing.DateTimeProvider;

import java.time.Instant;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;

public class FixedDateTimeProvider implements DateTimeProvider {

    private final Instant fixedInstant;

    public FixedDateTimeProvider(Instant fixedInstant) {
        this.fixedInstant = fixedInstant;
    }

    @Override
    public Optional<TemporalAccessor> getNow() {
        return Optional.of(fixedInstant);
    }
}

動作確認とテスト

Auditing機能が正しく動作することを確認するテストコードを実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.example.entity;

import com.example.config.JpaAuditingConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.AuditorAware;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

@DataJpaTest
@Import(JpaAuditingConfig.class)
class OrderAuditingTest {

    @Autowired
    private TestEntityManager entityManager;

    @MockitoBean
    private AuditorAware<String> auditorAware;

    @BeforeEach
    void setUp() {
        when(auditorAware.getCurrentAuditor()).thenReturn(Optional.of("testUser"));
    }

    @Test
    @DisplayName("新規エンティティ保存時に監査情報が自動設定される")
    void shouldSetAuditFieldsOnCreate() {
        // Given
        Order order = new Order();
        order.setOrderNumber("ORD-001");
        order.setTotalAmount(10000);
        order.setStatus(OrderStatus.PENDING);

        // When
        Order savedOrder = entityManager.persistFlushFind(order);

        // Then
        assertThat(savedOrder.getCreatedBy()).isEqualTo("testUser");
        assertThat(savedOrder.getUpdatedBy()).isEqualTo("testUser");
        assertThat(savedOrder.getCreatedAt()).isNotNull();
        assertThat(savedOrder.getUpdatedAt()).isNotNull();
        assertThat(savedOrder.getCreatedAt()).isEqualTo(savedOrder.getUpdatedAt());
    }

    @Test
    @DisplayName("エンティティ更新時に更新者と更新日時のみ変更される")
    void shouldUpdateOnlyLastModifiedFieldsOnUpdate() {
        // Given
        Order order = new Order();
        order.setOrderNumber("ORD-002");
        order.setTotalAmount(20000);
        order.setStatus(OrderStatus.PENDING);
        Order savedOrder = entityManager.persistFlushFind(order);

        String originalCreatedBy = savedOrder.getCreatedBy();
        var originalCreatedAt = savedOrder.getCreatedAt();

        // 更新者を変更
        when(auditorAware.getCurrentAuditor()).thenReturn(Optional.of("anotherUser"));

        // When
        savedOrder.setStatus(OrderStatus.CONFIRMED);
        entityManager.flush();
        entityManager.clear();

        Order updatedOrder = entityManager.find(Order.class, savedOrder.getId());

        // Then
        assertThat(updatedOrder.getCreatedBy()).isEqualTo(originalCreatedBy);
        assertThat(updatedOrder.getCreatedAt()).isEqualTo(originalCreatedAt);
        assertThat(updatedOrder.getUpdatedBy()).isEqualTo("anotherUser");
        assertThat(updatedOrder.getUpdatedAt()).isAfterOrEqualTo(originalCreatedAt);
    }
}

よくある誤解とアンチパターン

Spring Data JPA Auditingを使用する際に陥りやすい誤解とアンチパターンを解説します。

@EntityListenersの付与忘れ

問題: @MappedSuperclass@EntityListeners(AuditingEntityListener.class)を付与し忘れ、監査情報が設定されない。

1
2
3
4
5
6
7
// NG: @EntityListenersがない
@MappedSuperclass
public abstract class BaseEntity {
    @CreatedDate
    private Instant createdAt;
    // ...
}

解決策: 基底クラスに必ず@EntityListeners(AuditingEntityListener.class)を付与します。

1
2
3
4
5
6
7
8
// OK: @EntityListenersを付与
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
    @CreatedDate
    private Instant createdAt;
    // ...
}

AuditorAwareのBean登録忘れ

問題: @CreatedBy/@LastModifiedByを使用しているが、AuditorAwareがBeanとして登録されていないため、nullが設定される。

解決策: AuditorAware実装クラスに@Componentを付与するか、@Beanメソッドで登録します。

1
2
3
4
@Component
public class SecurityAuditorAware implements AuditorAware<String> {
    // ...
}

新規保存時のnullチェック漏れ

問題: 新規エンティティの保存時にAuditorAware.getCurrentAuditor()Optional.empty()を返すと、NOT NULL制約違反が発生する。

解決策: 常にデフォルト値を返すようにフォールバック処理を実装します。

1
2
3
4
5
6
7
8
@Override
public Optional<String> getCurrentAuditor() {
    return Optional.ofNullable(SecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName)
            .or(() -> Optional.of("SYSTEM")); // デフォルト値
}

@Transactionalとの併用時の注意

問題: サービスクラスの@Transactionalメソッド内でエンティティを変更しても、@LastModifiedDateが更新されない場合がある。

原因: Dirty Checkによる更新は@PreUpdateコールバックを発火しますが、エンティティの変更がない場合は更新されません。

1
2
3
4
5
6
7
// Dirty Checkで変更が検出されない場合、@PreUpdateは発火しない
@Transactional
public void process(Long orderId) {
    Order order = orderRepository.findById(orderId).orElseThrow();
    // 何も変更しない場合、@LastModifiedDateは更新されない
    // orderRepository.save(order); を呼んでも同様
}

@CreatedDateに手動で値を設定する

問題: @CreatedDateフィールドに手動で値を設定しようとしても、AuditingEntityListenerによって上書きされる。

解決策: 監査フィールドへの手動設定は避け、Auditing機能に委ねます。特殊な要件がある場合は、カスタムDateTimeProviderを実装します。

実践Tips

Tip 1: 論理削除との組み合わせ

論理削除フラグとAuditingを組み合わせる場合の基底クラス設計です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class SoftDeletableEntity extends BaseAuditableEntity {

    @Column(name = "deleted", nullable = false)
    private boolean deleted = false;

    @Column(name = "deleted_at")
    private Instant deletedAt;

    @Column(name = "deleted_by", length = 100)
    private String deletedBy;

    public boolean isDeleted() {
        return deleted;
    }

    public void markAsDeleted(String deletedBy) {
        this.deleted = true;
        this.deletedAt = Instant.now();
        this.deletedBy = deletedBy;
    }

    // Getters
    public Instant getDeletedAt() {
        return deletedAt;
    }

    public String getDeletedBy() {
        return deletedBy;
    }
}

Tip 2: DTOへのマッピング

監査情報をDTOに含める場合のパターンです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public record OrderResponse(
    Long id,
    String orderNumber,
    Integer totalAmount,
    String status,
    AuditInfo auditInfo
) {
    public static OrderResponse from(Order order) {
        return new OrderResponse(
            order.getId(),
            order.getOrderNumber(),
            order.getTotalAmount(),
            order.getStatus().name(),
            new AuditInfo(
                order.getCreatedBy(),
                order.getCreatedAt(),
                order.getUpdatedBy(),
                order.getUpdatedAt()
            )
        );
    }
}

public record AuditInfo(
    String createdBy,
    Instant createdAt,
    String updatedBy,
    Instant updatedAt
) {}

Tip 3: 非同期処理でのAuditorAware

@Asyncメソッド内ではSecurityContextHolderの情報が伝播されないため、SecurityContextHolder.setStrategyName()で戦略を変更するか、明示的にコンテキストを伝播させます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return new DelegatingSecurityContextAsyncTaskExecutor(executor);
    }
}

まとめ

Spring Data JPA Auditingは、エンティティの監査情報を自動的に管理するための強力な機能です。本記事で解説した内容を振り返ります。

項目 ポイント
@EnableJpaAuditing 設定クラスに付与してAuditing機能を有効化
@CreatedDate / @LastModifiedDate 日時の自動記録、Instant型の使用を推奨
@CreatedBy / @LastModifiedBy AuditorAwareの実装が必須
AuditorAware Spring Securityと連携して現在のユーザーを取得
@MappedSuperclass 監査カラムを基底クラスに共通化
@EntityListeners AuditingEntityListenerを指定して監査処理を有効化

監査機能は単なる技術要件ではなく、システムの信頼性とコンプライアンスを支える重要な基盤です。適切に実装することで、データの追跡可能性を確保し、問題発生時の原因特定を迅速に行えるようになります。

参考リンク