はじめに
JPA Auditing 機能を通じて、繰り返し発生する日時管理コードを削除し、生産性を高める方法を整理してみました。
問題の状況
開発時にはほぼすべてのテーブルで次のような共通メタデータフィールドが必要になります。
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
-
created_at
: データ作成時刻 -
updated_at
: データ最終更新時刻
作成日時や更新日時といったフィールドはどのように管理すればよいでしょうか?
通常、このような場合には2つのアプローチ方法で解決します。
コード(アプリケーション)側での責任
コード(アプリケーション)側で責任を持つ例を確認してみましょう。
@Entity
@Getter
@Setter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
/*
省略
*/
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
次のような User エンティティのコードがある場合、オブジェクトを生成すると、
作成・更新フィールドは以下のように管理されます。
public User createUser(String name) {
User user = new User();
user.setName(name);
/*
省略
*/
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
return user;
}
user オブジェクトの値を修正する際にも、次のようにメソッドを定義する必要があります。
public void updateName(String name){
this.name = name;
user.setUpdatedAt(LocalDateTime.now());
}
作成と削除、この2つの場合だけを見ても、繰り返し記述するコードが発生します。
// 繰り返し発生するコード
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
もしコード側で上記の例のように管理する場合、次のような問題が発生します。
コード側で管理する際に発生する問題
- ヒューマンエラーの可能性が増加
- もし開発者が作業中に該当コードを漏らしてしまった場合、データの一貫性が失われる可能性があります
- ボイラープレートコードの増加
- 繰り返し冗長なコードが記述されることで、開発者の可読性と保守性が低下します
- 関心の分離 (SoC, Separation of Concerns) の原則違反
- 日付管理ロジックはビジネスロジックではなく「技術的関心事」であるにもかかわらず、エンティティ内部でドメインロジックと一緒に管理されています
- 各ビジネスメソッドは 単一責任原則(SRP) も違反することになります
データベース側での責任
それでは、コード側ではなくデータベースに責任を持たせた場合はどうでしょうか?
次のように定義できます。
created_at timestamp default CURRENT_TIMESTAMP not null,
updated_at timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP,
これにより、以前のコードで発生した問題を解決できます。
public User createUser(String name) {
User user = new User();
user.setName(name);
/*
省略
*/
return user;
}
public void updateName(String name){
this.name = name;
}
このようにすれば、以前のコードで発生していた問題を解決することができます。
DBで管理する際に発生する問題
- DB依存性が高まる
- DBに依存する機能であるため、他のデータベースに移行する場合、動作方式が異なる可能性があります(実装方式、動作方式など)
- ビジネスロジックとの統合が難しい
- もし特定のドメインにおいて、他のフィールドが更新されても更新日時に反映させたくない場合、DBに依存していると制御が難しくなります
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
などのフィールドが変更されるのか把握できません。これはヒューマンエラーを誘発し、生産性低下につながります
- アプリケーションとデータベースの状態不整合
- DBの
ON UPDATE
機能でupdated_at
が変更された場合 - DBの値は新しい値に更新されますが...
- JAVA オブジェクトの
updateAT
フィールドは依然として以前の値を保持しています - つまり、DBとアプリケーション間でデータの整合性問題が発生します
- DBの
解決方法 : JPA Auditing
上記の問題を解決するために JPA Auditing 機能を活用することができます。
JPA Auditing とは?
- Spring Data JPA Auditing は、エンティティの作成/更新時に自動的に日付やユーザー情報を注入してくれる機能です
JPA Auditing の利点
- 開発者が直接
createdAt
、updatedAt
などのフィールドを管理する必要がありません - JPAエンティティライフサイクルイベントを通じて、コード側で即座に自動処理されます(つまりデータ整合性問題が解決されます)
- アプリケーション側で管理されるため、DB移行などのインフラ変更やテスト時の
mock
も自由に行えます - ボイラープレートコードがなく、コードに意図が明確に示され、予測可能なコードを書くことができます
適用方法
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
次のように @EnableJpaAuditing
を有効化し、
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
次のように共通の日時管理用の抽象クラスを作成します。
@EntityListeners(AuditingEntityListener.class)
このアノテーションは、JPAが自動的に発生させるライフサイクルイベントを AuditingEntityListener が検知し、特定のロジックを実行するように指定するアノテーションです。
つまり、JPAのライフサイクルイベントが発生するたびに AuditingEntityListener が動作するように登録するものであり、このリスナーは @CreatedDate
、@LastModifiedDate
フィールドに値を注入するロジックを実行します。
@Entity
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// その他のフィールドおよびメソッド
}
継承して使用します。
さらに追加で
@CreatedBy
@Column(updatable = false)
private String createdBy; // 作成したユーザー
@LastModifiedBy
private String lastModifiedBy; // 更新したユーザー
@CreatedBy
、@LastModifiedBy
アノテーションを活用すれば、Spring Security と連携してデータを作成・更新したユーザーを自動的に記録し、データの責任追跡性を高めることもできます。
適用事例
私の場合は、もう少し明確に表現し、さらに柔軟性(将来的に user エンティティで createAt
のようなカラムが別途管理される場合など)を考慮するため、次のように使用しています。