エンタープライズシステムにおいて、「誰が」「いつ」「何を」変更したかを追跡する監査ログは、セキュリティ要件やコンプライアンス対応に不可欠な機能です。Spring Bootでは、Spring Data JPAのAuditing機能による基本的な監査情報の自動記録と、Hibernate Enversによる完全な変更履歴の管理という2つのアプローチを組み合わせることで、堅牢な監査ログシステムを構築できます。
本記事では、監査ログの要件定義から、Spring Data JPAのAuditing機能の有効化、AuditorAwareの実装、そしてEnversによる履歴テーブルの自動生成とリビジョン管理まで、実践的な実装パターンを解説します。
実行環境と前提条件#
| 項目 |
バージョン |
| Java |
21 |
| Spring Boot |
3.4.1 |
| Spring Data JPA |
3.4.1 |
| Hibernate Envers |
6.6.4.Final |
| PostgreSQL |
16 |
前提条件として、Spring Boot Webプロジェクトが作成済みであることを想定しています。spring-boot-starter-data-jpa依存関係が含まれていれば問題ありません。
監査ログの要件定義#
監査ログを設計する際は、ビジネス要件とシステム要件の両面から考慮する必要があります。
基本的な監査要件(5W1H)#
監査ログでは以下の情報を記録することが求められます。
| 要素 |
説明 |
例 |
| Who(誰が) |
操作を実行したユーザー |
ユーザーID、ユーザー名 |
| When(いつ) |
操作が実行された日時 |
タイムスタンプ |
| What(何を) |
変更されたデータの内容 |
変更前後の値 |
| Where(どこで) |
変更されたリソース |
テーブル名、レコードID |
| Why(なぜ) |
変更の理由(オプション) |
操作種別(INSERT/UPDATE/DELETE) |
| How(どのように) |
変更の経緯(オプション) |
API経由、バッチ処理 |
監査レベルの分類#
監査ログは記録する詳細度によって以下のレベルに分類できます。
graph TD
A[監査ログの種類] --> B[レベル1: 基本監査]
A --> C[レベル2: フィールド監査]
A --> D[レベル3: 完全履歴監査]
B --> B1["作成者/更新者<br/>作成日時/更新日時"]
C --> C1["変更されたフィールド<br/>変更前後の値"]
D --> D1["全リビジョン履歴<br/>任意時点のスナップショット"]
| レベル |
説明 |
実装方法 |
| レベル1: 基本監査 |
作成者・更新者・日時のみ記録 |
Spring Data JPA Auditing |
| レベル2: フィールド監査 |
変更されたフィールドと値を記録 |
カスタム実装 or Envers |
| レベル3: 完全履歴監査 |
全変更履歴を別テーブルに保存 |
Hibernate Envers |
Spring Data JPA Auditingによる基本監査#
Spring Data JPAのAuditing機能を使用すると、エンティティの作成者・更新者・作成日時・更新日時を自動的に記録できます。
依存関係の追加#
build.gradleに以下の依存関係を追加します。
1
2
3
4
5
|
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
runtimeOnly 'org.postgresql:postgresql'
}
|
Auditing機能の有効化#
@EnableJpaAuditingアノテーションを設定クラスに追加してAuditing機能を有効化します。
1
2
3
4
5
6
7
8
9
|
package com.example.audit.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
|
監査用の基底エンティティクラス#
すべてのエンティティで共通して使用する監査フィールドを基底クラスにまとめます。
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.audit.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 BaseAuditEntity {
@CreatedBy
@Column(name = "created_by", nullable = false, updatable = false)
private String createdBy;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedBy
@Column(name = "updated_by", nullable = false)
private String updatedBy;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
// Getters and Setters
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public String getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}
|
各アノテーションの役割は以下のとおりです。
| アノテーション |
説明 |
@MappedSuperclass |
継承先エンティティにフィールドをマッピング |
@EntityListeners |
エンティティのライフサイクルイベントをリッスン |
@CreatedBy |
エンティティ作成者を自動設定 |
@CreatedDate |
エンティティ作成日時を自動設定 |
@LastModifiedBy |
最終更新者を自動設定 |
@LastModifiedDate |
最終更新日時を自動設定 |
AuditorAwareの実装#
@CreatedByと@LastModifiedByに設定する値を取得するため、AuditorAwareインターフェースを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package com.example.audit.config;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(context -> context.getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}
|
Spring Securityを使用していない場合や、システムユーザーとして記録したい場合は、以下のように実装します。
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.audit.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 RequestHeaderAuditorAware implements AuditorAware<String> {
private static final String DEFAULT_AUDITOR = "SYSTEM";
private static final String USER_ID_HEADER = "X-User-Id";
@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))
.or(() -> Optional.of(DEFAULT_AUDITOR));
}
}
|
エンティティへの適用#
基底クラスを継承してエンティティを作成します。
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.audit.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "products")
public class Product extends BaseAuditEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column
private String description;
// 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 Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
|
動作確認#
エンティティを保存すると、監査フィールドが自動的に設定されます。
1
2
3
4
5
6
7
8
9
10
|
Product product = new Product();
product.setName("Sample Product");
product.setPrice(1000);
productRepository.save(product);
// 保存後の状態
// created_by: "user123"
// created_at: "2026-01-04T08:00:00Z"
// updated_by: "user123"
// updated_at: "2026-01-04T08:00:00Z"
|
Hibernate Enversによる完全履歴監査#
Spring Data JPA Auditingは基本的な監査情報のみを記録しますが、Hibernate Enversを使用すると、エンティティの完全な変更履歴を別テーブルに自動保存できます。
Enversの概要#
graph LR
A[アプリケーション] --> B[Hibernate ORM]
B --> C[Envers]
C --> D[(メインテーブル<br/>products)]
C --> E[(監査テーブル<br/>products_AUD)]
C --> F[(リビジョンテーブル<br/>REVINFO)]
E --> FEnversは以下の機能を提供します。
| 機能 |
説明 |
| 自動履歴テーブル生成 |
_AUDサフィックスの監査テーブルを自動作成 |
| リビジョン管理 |
トランザクション単位でリビジョン番号を付与 |
| 履歴クエリ |
特定リビジョン時点のデータを取得可能 |
| 変更種別の記録 |
INSERT/UPDATE/DELETEを識別 |
依存関係の追加#
build.gradleにEnversの依存関係を追加します。
1
2
3
4
5
|
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.hibernate.orm:hibernate-envers'
runtimeOnly 'org.postgresql:postgresql'
}
|
エンティティへの@Auditedアノテーション#
監査対象のエンティティに@Auditedアノテーションを付与します。
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.audit.entity;
import jakarta.persistence.*;
import org.hibernate.envers.Audited;
@Entity
@Table(name = "products")
@Audited
public class Product extends BaseAuditEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column
private String description;
// Getters and Setters(省略)
}
|
自動生成されるテーブル構造#
Enversは以下のテーブルを自動生成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
-- リビジョン情報テーブル(自動生成)
CREATE TABLE REVINFO (
REV INTEGER NOT NULL PRIMARY KEY,
REVTSTMP BIGINT
);
-- 監査テーブル(自動生成)
CREATE TABLE products_AUD (
id BIGINT NOT NULL,
REV INTEGER NOT NULL,
REVTYPE SMALLINT,
name VARCHAR(255),
price INTEGER,
description VARCHAR(255),
created_by VARCHAR(255),
created_at TIMESTAMP,
updated_by VARCHAR(255),
updated_at TIMESTAMP,
PRIMARY KEY (id, REV),
FOREIGN KEY (REV) REFERENCES REVINFO(REV)
);
|
REVTYPEカラムには操作種別が記録されます。
| 値 |
操作種別 |
説明 |
| 0 |
ADD |
INSERT操作 |
| 1 |
MOD |
UPDATE操作 |
| 2 |
DEL |
DELETE操作 |
カスタムリビジョンエンティティ#
デフォルトのリビジョンエンティティでは、リビジョン番号とタイムスタンプのみが記録されます。ユーザー情報などの追加情報を記録するには、カスタムリビジョンエンティティを作成します。
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
|
package com.example.audit.entity;
import jakarta.persistence.*;
import org.hibernate.envers.RevisionEntity;
import org.hibernate.envers.RevisionNumber;
import org.hibernate.envers.RevisionTimestamp;
import java.time.Instant;
@Entity
@Table(name = "revision_info")
@RevisionEntity(CustomRevisionListener.class)
public class CustomRevisionEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@RevisionNumber
private Long id;
@RevisionTimestamp
@Column(name = "revision_timestamp")
private Instant revisionTimestamp;
@Column(name = "username")
private String username;
@Column(name = "ip_address")
private String ipAddress;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Instant getRevisionTimestamp() {
return revisionTimestamp;
}
public void setRevisionTimestamp(Instant revisionTimestamp) {
this.revisionTimestamp = revisionTimestamp;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
}
|
RevisionListenerの実装#
リビジョン作成時に追加情報を設定するリスナーを実装します。
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
|
package com.example.audit.entity;
import org.hibernate.envers.RevisionListener;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
public class CustomRevisionListener implements RevisionListener {
@Override
public void newRevision(Object revisionEntity) {
CustomRevisionEntity revision = (CustomRevisionEntity) revisionEntity;
// ユーザー名の設定
String username = getCurrentUsername();
revision.setUsername(username);
// IPアドレスの設定
String ipAddress = getCurrentIpAddress();
revision.setIpAddress(ipAddress);
}
private String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication.getName();
}
return "SYSTEM";
}
private String getCurrentIpAddress() {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
return attributes.getRequest().getRemoteAddr();
}
return "UNKNOWN";
}
}
|
履歴データの取得#
AuditReaderを使用して履歴データを取得できます。
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
|
package com.example.audit.service;
import com.example.audit.entity.CustomRevisionEntity;
import com.example.audit.entity.Product;
import jakarta.persistence.EntityManager;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.envers.query.AuditEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
public class AuditService {
private final EntityManager entityManager;
public AuditService(EntityManager entityManager) {
this.entityManager = entityManager;
}
/**
* 指定したエンティティのリビジョン一覧を取得
*/
public List<Number> getRevisions(Long productId) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
return auditReader.getRevisions(Product.class, productId);
}
/**
* 指定したリビジョン時点のエンティティを取得
*/
public Product getProductAtRevision(Long productId, Number revision) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
return auditReader.find(Product.class, productId, revision);
}
/**
* 指定したリビジョンの詳細情報を取得
*/
public CustomRevisionEntity getRevisionInfo(Number revision) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
return auditReader.findRevision(CustomRevisionEntity.class, revision);
}
/**
* エンティティの全履歴を取得(リビジョン情報付き)
*/
@SuppressWarnings("unchecked")
public List<Object[]> getProductHistory(Long productId) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
return auditReader.createQuery()
.forRevisionsOfEntity(Product.class, false, true)
.add(AuditEntity.id().eq(productId))
.addOrder(AuditEntity.revisionNumber().asc())
.getResultList();
}
}
|
履歴取得APIの実装#
履歴データを取得するREST APIを実装します。
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
|
package com.example.audit.controller;
import com.example.audit.dto.ProductHistoryResponse;
import com.example.audit.entity.CustomRevisionEntity;
import com.example.audit.entity.Product;
import com.example.audit.service.AuditService;
import org.hibernate.envers.RevisionType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/products")
public class ProductAuditController {
private final AuditService auditService;
public ProductAuditController(AuditService auditService) {
this.auditService = auditService;
}
@GetMapping("/{id}/revisions")
public ResponseEntity<List<Number>> getRevisions(@PathVariable Long id) {
List<Number> revisions = auditService.getRevisions(id);
return ResponseEntity.ok(revisions);
}
@GetMapping("/{id}/revisions/{revision}")
public ResponseEntity<Product> getProductAtRevision(
@PathVariable Long id,
@PathVariable Integer revision) {
Product product = auditService.getProductAtRevision(id, revision);
if (product == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(product);
}
@GetMapping("/{id}/history")
public ResponseEntity<List<ProductHistoryResponse>> getProductHistory(
@PathVariable Long id) {
List<Object[]> history = auditService.getProductHistory(id);
List<ProductHistoryResponse> response = history.stream()
.map(this::toHistoryResponse)
.collect(Collectors.toList());
return ResponseEntity.ok(response);
}
private ProductHistoryResponse toHistoryResponse(Object[] row) {
Product product = (Product) row[0];
CustomRevisionEntity revision = (CustomRevisionEntity) row[1];
RevisionType revisionType = (RevisionType) row[2];
return new ProductHistoryResponse(
product.getId(),
product.getName(),
product.getPrice(),
product.getDescription(),
revision.getId(),
revision.getRevisionTimestamp(),
revision.getUsername(),
revisionType.name()
);
}
}
|
履歴レスポンスDTO#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package com.example.audit.dto;
import java.time.Instant;
public record ProductHistoryResponse(
Long id,
String name,
Integer price,
String description,
Long revisionNumber,
Instant revisionTimestamp,
String modifiedBy,
String operationType
) {
}
|
期待される出力#
履歴取得APIを呼び出すと、以下のようなレスポンスが返されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
[
{
"id": 1,
"name": "Sample Product",
"price": 1000,
"description": "Initial description",
"revisionNumber": 1,
"revisionTimestamp": "2026-01-04T08:00:00Z",
"modifiedBy": "user123",
"operationType": "ADD"
},
{
"id": 1,
"name": "Sample Product",
"price": 1200,
"description": "Updated description",
"revisionNumber": 2,
"revisionTimestamp": "2026-01-04T09:30:00Z",
"modifiedBy": "admin",
"operationType": "MOD"
}
]
|
Enversの設定オプション#
application.ymlでEnversの動作をカスタマイズできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
spring:
jpa:
properties:
org:
hibernate:
envers:
# 監査テーブルのサフィックス(デフォルト: _AUD)
audit_table_suffix: _HISTORY
# リビジョン番号カラム名(デフォルト: REV)
revision_field_name: revision_id
# リビジョン種別カラム名(デフォルト: REVTYPE)
revision_type_field_name: revision_type
# 楽観的ロックのバージョンカラムを監査対象外にする
do_not_audit_optimistic_locking_field: true
# 削除時にデータを保存する(デフォルト: false)
store_data_at_delete: true
# 監査ストラテジー(有効期間方式を使用)
audit_strategy: org.hibernate.envers.strategy.ValidityAuditStrategy
# 有効期間終了リビジョンのカラム名
audit_strategy_validity_end_rev_field_name: revision_end
|
ValidityAuditStrategyの利点#
デフォルトの監査ストラテジーではサブクエリを使用するため、クエリパフォーマンスが低下する場合があります。ValidityAuditStrategyを使用すると、開始リビジョンと終了リビジョンを記録し、効率的なクエリが可能になります。
1
2
3
4
5
6
7
8
9
10
|
-- デフォルトストラテジーのクエリ(サブクエリ使用)
SELECT * FROM products_AUD
WHERE id = ? AND REV = (
SELECT MAX(REV) FROM products_AUD
WHERE REV <= ? AND id = ?
)
-- ValidityAuditStrategyのクエリ(範囲検索)
SELECT * FROM products_AUD
WHERE id = ? AND REV <= ? AND (revision_end > ? OR revision_end IS NULL)
|
特定フィールドの監査除外#
一部のフィールドを監査対象から除外したい場合は、@NotAuditedアノテーションを使用します。
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
|
package com.example.audit.entity;
import jakarta.persistence.*;
import org.hibernate.envers.Audited;
import org.hibernate.envers.NotAudited;
@Entity
@Table(name = "users")
@Audited
public class User extends BaseAuditEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String email;
// パスワードは監査対象外
@NotAudited
@Column(nullable = false)
private String password;
// 最終ログイン日時は頻繁に更新されるため監査対象外
@NotAudited
@Column(name = "last_login_at")
private Instant lastLoginAt;
// Getters and Setters(省略)
}
|
関連エンティティの監査#
関連エンティティも監査対象にする場合は、関連エンティティにも@Auditedを付与します。
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.audit.entity;
import jakarta.persistence.*;
import org.hibernate.envers.Audited;
@Entity
@Table(name = "orders")
@Audited
public class Order extends BaseAuditEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", nullable = false, unique = true)
private String orderNumber;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
// Getters and Setters(省略)
}
|
監査対象外のエンティティとの関連を持つ場合は、@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)を使用します。
1
2
3
4
|
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
@Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
private Category category; // Categoryは監査対象外
|
監査ログのベストプラクティス#
パフォーマンス考慮事項#
- 監査テーブルのインデックス設計: リビジョン番号とエンティティIDに適切なインデックスを設定
- パーティショニング: 大量の履歴データがある場合はテーブルパーティショニングを検討
- ValidityAuditStrategy: クエリパフォーマンスが重要な場合に採用
セキュリティ考慮事項#
- 監査テーブルへの直接アクセス制限: 監査テーブルへの書き込み権限を制限
- 機密情報の除外: パスワードなどの機密情報は
@NotAuditedで除外
- 監査ログ自体のログ出力: 監査ログへのアクセスもログに記録
運用考慮事項#
- 古い履歴データのアーカイブ: 定期的に古いデータをアーカイブテーブルに移動
- 監査ログのバックアップ: 監査ログは別途バックアップを取得
- モニタリング: 監査テーブルのサイズを監視
まとめ#
本記事では、Spring Boot REST APIにおける監査ログの実装方法を解説しました。
Spring Data JPA Auditingは、作成者・更新者・日時の基本的な監査情報を自動記録する軽量なソリューションです。一方、Hibernate Enversは、エンティティの完全な変更履歴を別テーブルに保存し、任意の時点のスナップショットを取得できる強力な機能を提供します。
要件に応じて、Spring Data JPA Auditingのみで十分な場合もあれば、Hibernate Enversと組み合わせて完全な監査証跡を実現する場合もあります。監査ログはコンプライアンス対応やセキュリティ監査において重要な役割を果たすため、適切な設計と実装を心がけてください。
参考リンク#