JPA Auditing － JPAで作成日・更新日を自動管理する方法

## はじめに

JPA Auditing 機能を通じて、繰り返し発生する日時管理コードを削除し、生産性を高める方法を整理してみました。

## 問題の状況

開発時にはほぼすべてのテーブルで次のような共通メタデータフィールドが必要になります。

```sql
    created_at                 TIMESTAMP     NOT NULL,
    updated_at                 TIMESTAMP     NOT NULL 
```
- `created_at` : データ作成時刻

- `updated_at` : データ最終更新時刻


作成日時や更新日時といったフィールドはどのように管理すればよいでしょうか？

通常、このような場合には2つのアプローチ方法で解決します。


## コード（アプリケーション）側での責任

コード（アプリケーション）側で責任を持つ例を確認してみましょう。

```java
@Entity
@Getter
@Setter 
public class User {

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

    private String name;

   /*
    省略
    */


    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
```
次のような User エンティティのコードがある場合、オブジェクトを生成すると、
作成・更新フィールドは以下のように管理されます。

```java
 public User createUser(String name) {
        User user = new User();
        user.setName(name);
         /*
        省略
        */
    
        user.setCreatedAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());

        return user;
}
```
user オブジェクトの値を修正する際にも、次のようにメソッドを定義する必要があります。

```java
public void updateName(String name){
    this.name = name;
    user.setUpdatedAt(LocalDateTime.now());
}
```
作成と削除、この2つの場合だけを見ても、繰り返し記述するコードが発生します。
```java
// 繰り返し発生するコード
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
```

もしコード側で上記の例のように管理する場合、次のような問題が発生します。

### コード側で管理する際に発生する問題

- ヒューマンエラーの可能性が増加
    - もし開発者が作業中に該当コードを漏らしてしまった場合、データの一貫性が失われる可能性があります
- ボイラープレートコードの増加
    - 繰り返し冗長なコードが記述されることで、開発者の可読性と保守性が低下します
- 関心の分離 (SoC, Separation of Concerns) の原則違反
    - 日付管理ロジックはビジネスロジックではなく「技術的関心事」であるにもかかわらず、エンティティ内部でドメインロジックと一緒に管理されています
    - 各ビジネスメソッドは **単一責任原則（SRP）** も違反することになります

## データベース側での責任

それでは、コード側ではなくデータベースに責任を持たせた場合はどうでしょうか？
次のように定義できます。

```sql
    created_at                 timestamp   default CURRENT_TIMESTAMP not null,
    updated_at                 timestamp   default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP,
```

これにより、以前のコードで発生した問題を解決できます。

```java
 public User createUser(String name) {
        User user = new User();
        user.setName(name);
      /*
        省略
        */
    
        return user;
}
```

```java
public void updateName(String name){
    this.name = name;
}
```
このようにすれば、以前のコードで発生していた問題を解決することができます。

### DBで管理する際に発生する問題

- DB依存性が高まる
    - DBに依存する機能であるため、他のデータベースに移行する場合、動作方式が異なる可能性があります（実装方式、動作方式など）
- ビジネスロジックとの統合が難しい
    - もし特定のドメインにおいて、他のフィールドが更新されても更新日時に反映させたくない場合、DBに依存していると制御が難しくなります

```java
public void addFollower(User user) {
    this.followers.add(user);
    this.followerCount += 1;

   // ここで updated_at は更新したくありません。
   // しかし DB の ON UPDATE CURRENT_TIMESTAMP を使用すると、強制的に updated_at が変更されてしまいます。
}
```

- テストが困難になる
    - アプリケーションレベルで時間を管理すれば、オブジェクトを簡単に `mock` して制御し、テストすることができます
    - しかしこれをDBで管理すると、テストはほぼ不可能になります
> 例えば、1日前に作成された投稿が正しく取得されるか？

- 情報の隠蔽が発生する
    - 開発者は通常、エンティティクラスやサービスコード（ドメイン）を見て動作を予測します
    - DB側にロジックが隠れていると、コードだけではどのタイミングで `updateAt` などのフィールドが変更されるのか把握できません。これはヒューマンエラーを誘発し、生産性低下につながります

:::note info
### 最小驚きの原則（Principle of least astonishment）

システムのすべての構成要素は、構文が示唆する通りに正確に動作すべきである。
:::

https://en.wikipedia.org/wiki/Principle_of_least_astonishment

- アプリケーションとデータベースの状態不整合
    - DBの `ON UPDATE` 機能で `updated_at` が変更された場合
    - DBの値は新しい値に更新されますが...
    - JAVA オブジェクトの `updateAT` フィールドは依然として以前の値を保持しています
    - つまり、DBとアプリケーション間でデータの整合性問題が発生します

## 解決方法 : JPA Auditing

上記の問題を解決するために **JPA Auditing** 機能を活用することができます。

### JPA Auditing とは？

- Spring Data JPA Auditing は、エンティティの作成／更新時に自動的に日付やユーザー情報を注入してくれる機能です

### JPA Auditing の利点

- 開発者が直接 `createdAt`、`updatedAt` などのフィールドを管理する必要がありません
- JPAエンティティライフサイクルイベントを通じて、コード側で即座に自動処理されます（つまりデータ整合性問題が解決されます）
- アプリケーション側で管理されるため、DB移行などのインフラ変更やテスト時の `mock` も自由に行えます
- ボイラープレートコードがなく、コードに意図が明確に示され、予測可能なコードを書くことができます

### 適用方法

```java
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
```
次のように `@EnableJpaAuditing` を有効化し、 

```java
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}
```

次のように共通の日時管理用の抽象クラスを作成します。

:::note info
`@EntityListeners(AuditingEntityListener.class)`  
このアノテーションは、JPAが自動的に発生させるライフサイクルイベントを AuditingEntityListener が検知し、特定のロジックを実行するように指定するアノテーションです。  
つまり、JPAのライフサイクルイベントが発生するたびに AuditingEntityListener が動作するように登録するものであり、このリスナーは `@CreatedDate`、`@LastModifiedDate` フィールドに値を注入するロジックを実行します。
:::

```java

@Entity
public class User extends BaseEntity {

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

    private String name;

    // その他のフィールドおよびメソッド
}

``` 
継承して使用します。

さらに追加で

```java
    @CreatedBy
    @Column(updatable = false)
    private String createdBy; // 作成したユーザー

    @LastModifiedBy
    private String lastModifiedBy; // 更新したユーザー
```
`@CreatedBy`、`@LastModifiedBy` アノテーションを活用すれば、Spring Security と連携してデータを作成・更新したユーザーを自動的に記録し、データの責任追跡性を高めることもできます。

https://docs.spring.io/spring-data/jpa/reference/auditing.html


## 適用事例

私の場合は、もう少し明確に表現し、さらに柔軟性（将来的に user エンティティで `createAt` のようなカラムが別途管理される場合など）を考慮するため、次のように使用しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/0a55925a-2f5c-4969-9619-95abd51b22a3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3815412/9affbb09-c2f0-4e7b-850f-9b35cda428ea.png)