0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Spring Boot】Hibernateで実装する論理削除(Soft Delete)

Last updated at Posted at 2025-08-30

バージョン補足

  • Hibernate 3.x〜6.2 : @Where(clause = ...) が一般的に利用されていた
  • Hibernate 6.3 (2023/09〜) : @Wheredeprecated になり、代わりに @SQLRestriction / @SQLJoinTableRestriction が推奨
  • Hibernate 6.4 (2023/12〜) : @SoftDelete が導入され、boolean フラグによる論理削除が最もシンプルに実装可能に
    • ただし、@SoftDelete は boolean 専用で、deleted_at のような日時型には対応していません

歴史的に多くの環境でまだ利用されている @Where を例に解説します
使用しているHibernateのバージョンに応じて、本記事の@Where@SQLRestrictionに読み替えて進めてください

はじめに

業務アプリでデータを「削除」する場合、物理削除(Hard Delete) でなく 論理削除(Soft Delete) を求められる場面は多々あります。
監査や復元の必要性があるからです。(もちろん参画しているプロジェクトの要件によって様々ですが...)

Hibernate には @SQLDelete@Where を組み合わせる方法があります。
この記事では softDelete()の実用的な実装方法を紹介します。

【注意】論理削除(Soft Delete)がアンチパターンか否かの議論は本記事では扱っておりません。顧客要件で実装せざるを得ないケースを想定しています

1. テーブル設計

PostgreSQL を例に deleted_at を追加します。

ALTER TABLE users
  ADD COLUMN deleted_at timestamptz NULL;

-- 削除されていないものだけ UNIQUE を効かせたい場合
-- ※これはPostgreSQLの部分インデックスですが、DBによって対応は異なってきます
CREATE UNIQUE INDEX ux_users_email_active
  ON users(email) WHERE deleted_at IS NULL;

2. エンティティ定義

@SQLDelete で DELETE を UPDATE に置き換え、
@Where で削除済みを自動的に除外します。

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

@Entity
@Table(name = "users")
@SQLDelete(sql = "UPDATE users SET deleted_at = now() WHERE id = ?")
@Where(clause = "deleted_at IS NULL")
public class User {

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

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

  @Column(nullable = false)
  private String name;

  private OffsetDateTime deletedAt;
}

これで userRepository.deleteById(id) を呼んでも 物理削除ではなく UPDATE が実行されます。
さらに findAll()findById()deleted_at IS NULL 条件付きで動くため、削除済みデータは返りません。

3. リポジトリ(復元・物理削除)

復元や物理削除を行いたい場合は、明示的にメソッドを追加します。

public interface UserRepository extends JpaRepository<User, Long> {

  // 論理削除データの復元
  @Modifying
  @Query("update User u set u.deletedAt = null where u.id = :id")
  void restoreById(@Param("id") Long id);

  // 物理削除
  @Modifying
  @Query(value = "delete from users where id = :id", nativeQuery = true)
  void hardDeleteById(@Param("id") Long id);
}

4. サービス層(softDelete)

サービス層で softDelete() のように、論理削除であることを明示した命名をすることで、呼び出し側が意図を理解しやすくなります。

@Service
@RequiredArgsConstructor
@Transactional
public class UserCommandService {
  private final UserRepository users;

  // 論理削除
  public void softDelete(Long id) {
    users.deleteById(id); // @SQLDelete により UPDATE に置き換わる
  }

  // 論理削除データの復元 ※必要があれば
  public void restore(Long id) {
    users.restoreById(id);
  }

  // 物理削除 ※必要があれば
  public void hardDelete(Long id) {
    users.hardDeleteById(id);
  }
}
// 呼び出し例。id=1に対する操作
userService.softDelete(1L);
userService.restore(1L);
userService.hardDelete(1L);

(飽くまで私の経験則なのですが...)業務システムだと、データの復元や削除済データの調査は保守・運用担当者が直接SQLを叩く場面が少なくないので、restorehardDeleteを実装する場面はあまりないかもしれません。

5. SELECT の挙動

userRepository.findAll() を実行すると、自動的に deleted_at IS NULL が付きます。

select u.id, u.email, u.name, u.deleted_at
from users u
where u.deleted_at is null;

@Where により「削除済みを除外する」挙動が保証されるため
アプリ側で毎回条件を書く必要はありません。

6. 実務での注意点

ユニーク制約の衝突

削除済みが残るため、email の UNIQUE が衝突する可能性あり。
→ PostgreSQL なら部分インデックスで対応。

集計・監査(削除済みも含めたい場合)

通常の JPQL/HQL クエリは、エンティティに付与した @Where(clause = "deleted_at IS NULL")常に有効 になります。
そのため、たとえば次のコードを書いても:

@Query("select count(u) from User u")
long countAll();

実際に発行される SQL は:

select count(u.id) from users u
where u.deleted_at is null;

→ 論理削除済みの行は自動的に除外されます。

削除済みも含めた集計をしたい場合は、nativeQuery を使うのが確実です。

@Query(value = "select count(*) from users", nativeQuery = true)
long countAllUsers();

これなら deleted_at IS NULL が付かず、削除済みも含めた件数を取得できます。

※ もう一つの方法として Hibernate の @Filter を使い、セッション単位で「削除済みも含める」切替を行う方法もあります。ただし入門向けには nativeQuery の方がシンプルです。

インデックス設計

deleted_at IS NULL 条件が必ず入るため、これを考慮したインデックスが必要です。
例:

CREATE INDEX idx_users_deleted_at ON users(deleted_at);
CREATE UNIQUE INDEX ux_users_email_active
  ON users(email) WHERE deleted_at IS NULL;

→ 前者はクエリの性能改善に、後者はユニーク制約の衝突回避に有効です。

まとめ

  • 論理削除は@SQLDelete + @Where で実装できる
  • SELECT は自動的に削除済みを除外(クラスに付けた @Where が効く)
  • サービス層に softDelete() / restore() / hardDelete() を明示すると実務でわかりやすいと思う
  • UNIQUE制約・集計・インデックス設計に注意する

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?