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, @LastModifiedDateAuditingEntityListenerは以下の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を指定して監査処理を有効化 |
監査機能は単なる技術要件ではなく、システムの信頼性とコンプライアンスを支える重要な基盤です。適切に実装することで、データの追跡可能性を確保し、問題発生時の原因特定を迅速に行えるようになります。
参考リンク#