エラーについて
データベースの更新を実行した際、成功メッセージではなく、コンソールに javax.persistence.TransactionRequiredException が出力されるという経験は、おそらく誰にでもあるでしょう。これは、JPAがデータの整合性に対して厳格であるために発生します。JPAは、更新や削除といった「書き込み」操作を実行する際、それらをラップするための保証されたトランザクションコンテキストが存在しない限り、実行を拒否します。
javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread
トランザクションはセーフティネットのようなものだと考えてください。これがないと、Hibernateは何かに問題が発生した際に、いつ変更をコミットすべきか、あるいはいつロールバックすべきかを判断できません。このネットの外でデータを変更しようとすると、JPAは非常ブレーキをかけます。
なぜこのエラーが発生するのか?
Springにおける EntityManager は共有プロキシです。リポジトリメソッドを呼び出すと、このプロキシは現在のスレッドに関連付けられたアクティブなトランザクションを探します。もしトランザクションが見つからず、かつデータを変更しようとしている場合、この例外がスローされます。主な原因は、通常以下の4つのシナリオのいずれかです:
- アノテーションの不足: サービスメソッドに
@Transactionalを付け忘れている。 - 自己呼び出し(Self-Invocation)の罠: 同じクラス内の別のメソッドからトランザクションメソッドを呼び出している。
- 手動クエリ: カスタムの
@Modifyingクエリを使用しているが、トランザクションの境界を定義していない。 - スレッドの境界: (
@AsyncやCompletableFutureを介して)新しいスレッドを開始したが、トランザクションがそのスレッドに引き継がれていない。
ステップバイステップの修正方法
1. サービスロジックをラップする
最も簡単な修正方法は、サービスレイヤーに @Transactional を配置することです。これにより、メソッドが開始された瞬間にトランザクションを開始し、メソッドが正常に終了した直後にコミットするようSpringに指示できます。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void deactivateUser(Long userId) {
// Springがここでトランザクションがアクティブであることを保証します
userRepository.updateStatus(userId, "INACTIVE");
}
}
2. カスタム @Modifying クエリの処理
JpaRepository でカスタムの UPDATE または DELETE クエリを記述する場合、Spring Data JPAはそれが自動的にトランザクションの一部であるとは見なしません。明示的に指定する必要があります。
誤った方法:
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying
@Query("UPDATE User u SET u.lastLogin = NOW() WHERE u.id = :id")
void updateLoginTimestamp(Long id);
}
正しい方法:
リポジトリに @Transactional を追加することもできますが、通常は複雑なビジネスロジックを管理するためにサービスレイヤーに保持するのが一般的です。リポジトリを単独で使用する場合は、次のように追加します:
public interface UserRepository extends JpaRepository<User, Long> {
@Transactional
@Modifying
@Query("UPDATE User u SET u.lastLogin = NOW() WHERE u.id = :id")
void updateLoginTimestamp(Long id);
}
3. 自己呼び出しループの解消
これはSpringトランザクションにおける「サイレントキラー」です。SpringはAOPプロキシを使用してトランザクションを管理します。同じクラス内で メソッドA が メソッドB を呼び出すと、そのプロキシをバイパスしてしまいます。これは、玄関に警備員がいるのに、内部の廊下を通って金庫室に向かうようなものです。警備員(プロキシ)はあなたの姿を見ることができません。
@Service
public class OrderService {
public void processOrder(Long id) {
// この呼び出しはプロキシをバイパスします!以下の @Transactional は無視されます。
saveOrder(id);
}
@Transactional
public void saveOrder(Long id) {
repository.updateStatus(id, "PROCESSED");
}
}
修正方法: 常にエントリポイントとなるメソッド(processOrder)にアノテーションを付けるか、プロキシが機能するようにトランザクションロジックを別のサービスクラスに移動します。
4. バックグラウンドスレッドでのトランザクション
トランザクションはスレッドに紐付いています。@Async を使用する場合、新しいスレッドは呼び出し元のトランザクションを参照できません。@Async メソッド自体に @Transactional をマークする必要があります。
@Async
@Transactional
public void processInBackground(Long entityId) {
// 新しいスレッドが独自のトランザクションを開始するため、これは機能します
repository.deleteOldRecords(entityId);
}
修正を確認する方法
ただ動くことを願うのではなく、検証しましょう。application.properties に以下の行を追加することで、トランザクションの動作を追跡できます:
logging.level.org.springframework.orm.jpa.JpaTransactionManager=DEBUG
コードを実行する際、Creating new transaction や Initiating transaction commit というログを探してください。データベース呼び出しの周囲にこれらのログが表示されない場合、エラーが解消されたとしても(特定のHibernate設定ではエラーが出ずにデータが失われることがあります)、トランザクションはアクティブではありません。
予防のためのプロのアドバイス
- 読み取り専用をデフォルトにする: クラスレベルで
@Transactional(readOnly = true)を使用し、実際にデータを変更するメソッドで標準の@Transactionalでオーバーライドします。これにより、Hibernateのダーティチェックをスキップしてパフォーマンスを向上させることができます。 - サービスレベルの境界: トランザクションはサービスレイヤーに保持してください。コントローラーに置くのは遅すぎますし、リポジトリに置くのは粒度が細かすぎることが多いです。
- ログを監視する:
TransactionRequiredExceptionが発生した場合、まず最初に確認すべきは、同じクラス内のメソッドを呼び出していないかということです。90%の場合、自己呼び出しが原因です。

