はじめに

Spring Data JPAを使った開発において、データの削除方式は重要な設計判断の一つです。特に「ソフトデリート(論理削除)」は、データの保全性や監査要件を満たすために多くのエンタープライズシステムで採用されています。

本記事では、Spring Data JPAにおけるソフトデリートの実装方法を詳しく解説します。Hibernateが提供する@SQLDeleteアノテーションによるDELETE文のUPDATEへの置き換え、@Where(Hibernate 6.3以降は@SQLRestriction)による自動フィルタリングなど、論理削除を実現するための具体的なテクニックを紹介します。

実行環境・前提条件

本記事のコードは以下の環境で動作確認しています。

項目 バージョン
Java 17以上
Spring Boot 3.4.x
Hibernate 6.6.x
データベース PostgreSQL 15 / MySQL 8.0

依存関係(build.gradle)は以下の通りです。

1
2
3
4
5
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'org.postgresql:postgresql'
    // または runtimeOnly 'com.mysql:mysql-connector-j'
}

物理削除と論理削除(ソフトデリート)の違い

物理削除(Hard Delete)

物理削除は、データベースからレコードを完全に削除する方式です。

1
DELETE FROM users WHERE id = 1;

特徴は以下の通りです。

  • ストレージを即座に解放できる
  • データは完全に消失し、復旧不可能
  • 外部キー制約による整合性維持が必要

論理削除(Soft Delete / ソフトデリート)

論理削除は、削除フラグや削除日時を更新することで、データを「削除済み」としてマークする方式です。

1
UPDATE users SET deleted_at = NOW() WHERE id = 1;

特徴は以下の通りです。

  • データの復旧が可能
  • 監査証跡や履歴の保持に有効
  • クエリに削除フラグの条件が必要
  • データ量が増加しやすい

選定基準

以下の観点から削除方式を選定します。

観点 物理削除が適切 論理削除が適切
監査要件 監査が不要 削除履歴の保持が必要
データ復旧 復旧の必要なし 誤削除時の復旧が必要
法的要件 完全削除が必要(GDPR等) 一定期間の保持義務あり
参照整合性 厳密な整合性が必要 関連データへの影響を最小化したい
パフォーマンス 大量データでのクエリ性能重視 データ量増加を許容できる

deleted_atカラム設計と実装例

カラム設計のパターン

論理削除を実現するためのカラム設計には、主に3つのパターンがあります。

graph LR
    A[論理削除カラム設計] --> B[booleanフラグ]
    A --> C[削除日時]
    A --> D[複合パターン]
    
    B --> B1["deleted: boolean"]
    C --> C1["deleted_at: timestamp"]
    D --> D1["deleted: boolean + deleted_at + deleted_by"]

パターン1: booleanフラグ

1
ALTER TABLE users ADD COLUMN deleted BOOLEAN DEFAULT FALSE NOT NULL;

最もシンプルですが、削除日時や削除者の情報を持てません。

パターン2: 削除日時(推奨)

1
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL;

deleted_atがNULLでなければ削除済みと判定します。削除日時も記録でき、実用性が高い方式です。

パターン3: 複合パターン

1
2
3
ALTER TABLE users ADD COLUMN deleted BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL;
ALTER TABLE users ADD COLUMN deleted_by VARCHAR(100) NULL;

監査要件が厳しい場合に採用します。

エンティティの基底クラス設計

再利用性を高めるため、論理削除機能を基底クラスとして実装します。

 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
package com.example.domain.entity;

import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import java.time.Instant;

@MappedSuperclass
public abstract class SoftDeletableEntity {

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

    public Instant getDeletedAt() {
        return deletedAt;
    }

    public void setDeletedAt(Instant deletedAt) {
        this.deletedAt = deletedAt;
    }

    public boolean isDeleted() {
        return deletedAt != null;
    }

    public void markAsDeleted() {
        this.deletedAt = Instant.now();
    }

    public void restore() {
        this.deletedAt = null;
    }
}

@SQLDeleteによるDELETE文のUPDATEへの置き換え

@SQLDeleteの基本

Hibernateの@SQLDeleteアノテーションを使用すると、EntityManager.remove()やSpring Data JPAのdeleteById()が呼び出されたときに、実際に発行されるSQL文をカスタマイズできます。

 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
package com.example.domain.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.SQLDelete;
import java.time.Instant;

@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
public class User extends SoftDeletableEntity {

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

    @Column(name = "email", nullable = false, unique = true)
    private String email;

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

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

    @PrePersist
    protected void onCreate() {
        this.createdAt = Instant.now();
    }

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

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }

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

    public Instant getCreatedAt() {
        return createdAt;
    }
}

動作確認

以下のようにリポジトリから削除を実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Service
@Transactional
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void deleteUser(Long userId) {
        // 内部的には DELETE ではなく UPDATE が発行される
        userRepository.deleteById(userId);
    }
}

実際に発行されるSQLは以下のようになります。

1
2
-- deleteById(1) 実行時
UPDATE users SET deleted_at = NOW() WHERE id = 1

複数テーブルの継承時の注意点

@SQLDeleteは継承されないため、各エンティティクラスで個別に指定する必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Entity
@Table(name = "orders")
@SQLDelete(sql = "UPDATE orders SET deleted_at = NOW() WHERE id = ?")
public class Order extends SoftDeletableEntity {
    // ...
}

@Entity
@Table(name = "products")
@SQLDelete(sql = "UPDATE products SET deleted_at = NOW() WHERE id = ?")
public class Product extends SoftDeletableEntity {
    // ...
}

@Where/@SQLRestrictionによる自動フィルタリング

@Whereアノテーション(Hibernate 6.3未満)

@Whereアノテーションを使用すると、エンティティに対するすべてのクエリに自動的にWHERE条件が追加されます。

1
2
3
4
5
6
7
@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
@Where(clause = "deleted_at IS NULL")
public class User extends SoftDeletableEntity {
    // ...
}

@SQLRestrictionアノテーション(Hibernate 6.3以降、推奨)

Hibernate 6.3以降では、@Whereは非推奨となり、@SQLRestrictionが推奨されています。機能は同等ですが、より明確な命名となっています。

 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.domain.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import java.time.Instant;

@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
public class User extends SoftDeletableEntity {

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

    @Column(name = "email", nullable = false)
    private String email;

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

    // ...
}

自動フィルタリングの効果

@SQLRestrictionを設定すると、以下のようにすべてのクエリに条件が自動付与されます。

1
2
3
4
5
// Repository
public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);
    Optional<User> findByEmail(String email);
}
1
2
3
4
5
6
7
8
-- findAll() 実行時
SELECT * FROM users WHERE deleted_at IS NULL

-- findByName("田中") 実行時
SELECT * FROM users WHERE name = '田中' AND deleted_at IS NULL

-- findById(1) 実行時
SELECT * FROM users WHERE id = 1 AND deleted_at IS NULL

関連エンティティへの適用

関連エンティティにも@SQLRestrictionを適用することで、関連データの取得時にも自動フィルタリングが機能します。

 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
@Entity
@Table(name = "posts")
@SQLDelete(sql = "UPDATE posts SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
public class Post extends SoftDeletableEntity {

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

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "content", columnDefinition = "TEXT")
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments = new ArrayList<>();

    // Getters and Setters
}

@Entity
@Table(name = "comments")
@SQLDelete(sql = "UPDATE comments SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
public class Comment extends SoftDeletableEntity {

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

    @Column(name = "content", nullable = false)
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id", nullable = false)
    private Post post;

    // Getters and Setters
}

削除済みデータの取得方法

@SQLRestrictionを使用すると通常のクエリでは削除済みデータを取得できません。管理画面での復元機能や監査目的で削除済みデータを取得する方法を紹介します。

方法1: ネイティブクエリを使用する

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public interface UserRepository extends JpaRepository<User, Long> {

    // 削除済みを含むすべてのユーザーを取得
    @Query(value = "SELECT * FROM users", nativeQuery = true)
    List<User> findAllIncludingDeleted();

    // 削除済みユーザーのみを取得
    @Query(value = "SELECT * FROM users WHERE deleted_at IS NOT NULL", nativeQuery = true)
    List<User> findAllDeleted();

    // 特定IDの削除済みユーザーを取得
    @Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
    Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
}

方法2: EntityManagerを直接使用する

 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
@Repository
public class UserRepositoryCustomImpl implements UserRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<User> findAllIncludingDeleted() {
        return entityManager
            .createNativeQuery("SELECT * FROM users", User.class)
            .getResultList();
    }

    @Override
    public Optional<User> findDeletedById(Long id) {
        List<User> results = entityManager
            .createNativeQuery(
                "SELECT * FROM users WHERE id = :id AND deleted_at IS NOT NULL",
                User.class
            )
            .setParameter("id", id)
            .getResultList();
        return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
    }
}

方法3: Hibernate Filterを使用する(高度な制御が必要な場合)

Hibernate Filterを使用すると、動的にフィルタリングのON/OFFを切り替えられます。

1
2
3
4
5
6
7
8
@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
@FilterDef(name = "deletedFilter", parameters = @ParamDef(name = "isDeleted", type = Boolean.class))
@Filter(name = "deletedFilter", condition = "deleted_at IS NULL OR :isDeleted = true")
public class User extends SoftDeletableEntity {
    // ...
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Service
public class UserQueryService {

    @PersistenceContext
    private EntityManager entityManager;

    public List<User> findAllWithDeletedOption(boolean includeDeleted) {
        Session session = entityManager.unwrap(Session.class);
        
        if (!includeDeleted) {
            session.enableFilter("deletedFilter").setParameter("isDeleted", false);
        }
        
        return entityManager
            .createQuery("SELECT u FROM User u", User.class)
            .getResultList();
    }
}

方法4: 別エンティティを定義する(推奨パターン)

管理者向けに@SQLRestrictionを持たない別エンティティを定義する方法もあります。

 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
// 通常のエンティティ(削除済みを除外)
@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
public class User extends SoftDeletableEntity {
    // ...
}

// 管理者用エンティティ(すべてのデータにアクセス可能)
@Entity
@Table(name = "users")
@Immutable // 読み取り専用
public class UserAdmin extends SoftDeletableEntity {

    @Id
    private Long id;

    @Column(name = "email")
    private String email;

    @Column(name = "name")
    private String name;

    // Gettersのみ
}
1
2
3
public interface UserAdminRepository extends JpaRepository<UserAdmin, Long> {
    List<UserAdmin> findByDeletedAtIsNotNull();
}

削除済みデータの復元

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
@Transactional
public class UserAdminService {

    private final EntityManager entityManager;

    public UserAdminService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public void restoreUser(Long userId) {
        int updated = entityManager
            .createNativeQuery("UPDATE users SET deleted_at = NULL WHERE id = :id")
            .setParameter("id", userId)
            .executeUpdate();
        
        if (updated == 0) {
            throw new EntityNotFoundException("User not found: " + userId);
        }
    }
}

ソフトデリートの注意点

UNIQUE制約の問題と解決策

論理削除を使用すると、UNIQUE制約で問題が発生することがあります。

sequenceDiagram
    participant App as アプリケーション
    participant DB as データベース
    
    Note over DB: users テーブル<br/>email UNIQUE制約
    
    App->>DB: INSERT user@example.com (id=1)
    DB-->>App: 成功
    
    App->>DB: DELETE id=1 (論理削除)
    Note over DB: deleted_at = NOW()
    DB-->>App: 成功
    
    App->>DB: INSERT user@example.com (id=2)
    DB-->>App: UNIQUE制約違反!
    Note over App,DB: 削除済みでも同じemailを<br/>再登録できない

解決策1: 部分インデックス(PostgreSQL)

1
2
3
4
-- 削除されていないレコードのみにUNIQUE制約を適用
CREATE UNIQUE INDEX users_email_unique_not_deleted 
ON users (email) 
WHERE deleted_at IS NULL;

解決策2: 複合UNIQUE制約

1
2
3
4
-- emailとdeleted_atの組み合わせでUNIQUE
-- NULL同士は等しくないとみなされるDBでのみ有効
ALTER TABLE users ADD CONSTRAINT users_email_deleted_unique 
UNIQUE (email, deleted_at);

解決策3: 削除時にユニーク値を変更

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Entity
@Table(name = "users")
@SQLDelete(sql = """
    UPDATE users 
    SET deleted_at = NOW(), 
        email = CONCAT(email, '_deleted_', id) 
    WHERE id = ?
    """)
@SQLRestriction("deleted_at IS NULL")
public class User extends SoftDeletableEntity {
    // ...
}

パフォーマンスとインデックス設計

論理削除を採用すると、テーブルのデータ量が増加し続けるため、適切なインデックス設計が重要です。

推奨インデックス

1
2
3
4
5
6
7
8
9
-- 削除フラグを含む複合インデックス
CREATE INDEX idx_users_email_deleted ON users (email, deleted_at);

-- 削除されていないレコードのみの部分インデックス
CREATE INDEX idx_users_active ON users (id) WHERE deleted_at IS NULL;

-- 削除日時での検索用(バッチ処理向け)
CREATE INDEX idx_users_deleted_at ON users (deleted_at) 
WHERE deleted_at IS NOT NULL;

クエリプランの確認

1
2
EXPLAIN ANALYZE 
SELECT * FROM users WHERE email = 'test@example.com' AND deleted_at IS NULL;

データアーカイブ戦略

論理削除されたデータは定期的にアーカイブすることを検討します。

 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
@Service
@Transactional
public class DataArchiveService {

    private final EntityManager entityManager;

    public DataArchiveService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    /**
     * 90日以上前に削除されたデータをアーカイブテーブルに移動
     */
    @Scheduled(cron = "0 0 2 * * *") // 毎日2時に実行
    public void archiveOldDeletedData() {
        Instant threshold = Instant.now().minus(90, ChronoUnit.DAYS);
        
        // アーカイブテーブルにコピー
        entityManager.createNativeQuery("""
            INSERT INTO users_archive 
            SELECT * FROM users 
            WHERE deleted_at IS NOT NULL AND deleted_at < :threshold
            """)
            .setParameter("threshold", threshold)
            .executeUpdate();
        
        // 元テーブルから物理削除
        entityManager.createNativeQuery("""
            DELETE FROM users 
            WHERE deleted_at IS NOT NULL AND deleted_at < :threshold
            """)
            .setParameter("threshold", threshold)
            .executeUpdate();
    }
}

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

アンチパターン1: すべてのテーブルに論理削除を適用する

論理削除は万能ではありません。以下のようなテーブルには物理削除が適切です。

  • 一時的なデータ(セッション、キャッシュ)
  • 大量の時系列データ(ログ、メトリクス)
  • 個人情報保護法により完全削除が必要なデータ

アンチパターン2: @SQLRestrictionを過信する

@SQLRestrictionはHibernate経由のクエリにのみ適用されます。以下のケースでは効果がありません。

1
2
3
4
5
6
7
// NG: ネイティブクエリには適用されない
@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
List<User> findByNameNative(@Param("name") String name);

// OK: JPQLには適用される
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByNameJpql(@Param("name") String name);

アンチパターン3: 関連エンティティの削除を忘れる

親エンティティを論理削除しても、子エンティティは自動的に論理削除されません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Service
@Transactional
public class UserService {

    private final UserRepository userRepository;
    private final PostRepository postRepository;

    // 推奨: 関連エンティティも一緒に論理削除
    public void deleteUserWithPosts(Long userId) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new EntityNotFoundException("User not found"));
        
        // 関連する投稿も論理削除
        postRepository.softDeleteByUserId(userId);
        
        // ユーザーを論理削除
        userRepository.delete(user);
    }
}
1
2
3
4
5
6
public interface PostRepository extends JpaRepository<Post, Long> {
    
    @Modifying
    @Query("UPDATE Post p SET p.deletedAt = CURRENT_TIMESTAMP WHERE p.user.id = :userId")
    void softDeleteByUserId(@Param("userId") Long userId);
}

アンチパターン4: 削除済みデータのキャッシュ問題

2次キャッシュを使用している場合、論理削除後もキャッシュにデータが残る可能性があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Service
@Transactional
public class UserService {

    private final UserRepository userRepository;
    private final EntityManager entityManager;

    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
        
        // キャッシュをクリア
        entityManager.getEntityManagerFactory()
            .getCache()
            .evict(User.class, userId);
    }
}

まとめと実践Tips

まとめ

Spring Data JPAでソフトデリート(論理削除)を実装する際の主要なポイントは以下の通りです。

  1. @SQLDeleteでDELETE文をUPDATE文に置き換える
  2. @SQLRestriction(Hibernate 6.3以降)で自動フィルタリングを適用する
  3. 削除済みデータの取得にはネイティブクエリや専用エンティティを使用する
  4. UNIQUE制約は部分インデックスで対応する
  5. 適切なインデックス設計とデータアーカイブ戦略を検討する

実践Tips

Tip 1: 共通基底クラスの活用

プロジェクト全体で一貫した論理削除を実現するため、基底クラスを活用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@MappedSuperclass
public abstract class SoftDeletableEntity {

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

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

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

    // Getters...
}

Tip 2: テスト時の注意点

テストでは削除済みデータも検証できるようにします。

 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
@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void softDeleteShouldSetDeletedAt() {
        // Given
        User user = new User();
        user.setEmail("test@example.com");
        user.setName("Test User");
        user = userRepository.save(user);
        Long userId = user.getId();

        // When
        userRepository.deleteById(userId);
        entityManager.flush();
        entityManager.clear();

        // Then: 通常のfindByIdでは取得できない
        assertThat(userRepository.findById(userId)).isEmpty();

        // Then: ネイティブクエリで削除済みを確認
        User deletedUser = (User) entityManager.getEntityManager()
            .createNativeQuery("SELECT * FROM users WHERE id = :id", User.class)
            .setParameter("id", userId)
            .getSingleResult();
        
        assertThat(deletedUser.getDeletedAt()).isNotNull();
    }
}

Tip 3: 監査機能との統合

Spring Data JPAの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
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableSoftDeletableEntity {

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

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

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

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

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

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

    // ...
}

参考リンク