タイトル: Caused by: java.lang.RuntimeException: Executing an update/delete query
SEOタイトル: Spring Data JPA「TransactionRequiredException: Executing an
| この記事の要点 |
|
エラー内容
Spring Data JPA のリポジトリで @Query + UPDATE/DELETE を実行すると以下が発生:
Caused by: java.lang.RuntimeException: Executing an update/delete query;
nested exception is javax.persistence.TransactionRequiredException:
Executing an update/delete query
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(...)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(...)
at ...
原因
JPA の仕様では UPDATE / DELETE / INSERT クエリは必ずトランザクション内で実行する必要があります。SELECT は不要ですが、データ変更系には明示的なトランザクション境界が要求されます。
Spring Data JPA はデフォルトでリポジトリメソッドに read-only トランザクションを開きますが、@Modifying 付きの更新クエリには別途指定が必要です。
発生する典型コード
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// ❌ NG: @Modifying と @Transactional がない
@Query("UPDATE User u SET u.lastLoginAt = :now WHERE u.id = :id")
void updateLastLogin(@Param("id") Long id, @Param("now") LocalDateTime now);
}
@Service
public class LoginService {
@Autowired
private UserRepository userRepository;
public void onLogin(Long userId) {
// ↓ ここで TransactionRequiredException
userRepository.updateLastLogin(userId, LocalDateTime.now());
}
}
対処1: リポジトリで @Modifying + @Transactional
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying // ★ UPDATE/DELETE クエリには必須
@Transactional // ★ 更新トランザクションを開く
@Query("UPDATE User u SET u.lastLoginAt = :now WHERE u.id = :id")
void updateLastLogin(@Param("id") Long id, @Param("now") LocalDateTime now);
}
@Modifying はこのクエリが SELECT ではないことを Spring に伝えます。これが無いと「結果セットが返ってこない」エラーになります。
@Transactional はメソッド呼び出し時にトランザクションを開始します。
対処2: Service 層に @Transactional(推奨)
リポジトリではなく、Service 層でトランザクション境界を引く方が設計上きれい。複数の更新を 1 トランザクションでまとめられます:
@Service
public class LoginService {
@Autowired
private UserRepository userRepository;
@Autowired
private LoginLogRepository loginLogRepository;
@Transactional // ★ Service のメソッド単位でトランザクション
public void onLogin(Long userId) {
userRepository.updateLastLogin(userId, LocalDateTime.now());
loginLogRepository.save(new LoginLog(userId, LocalDateTime.now()));
// 両方コミット or 両方ロールバック
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying // ★ これは依然として必要
@Query("UPDATE User u SET u.lastLoginAt = :now WHERE u.id = :id")
void updateLastLogin(@Param("id") Long id, @Param("now") LocalDateTime now);
}
@Modifying のオプション
| オプション | 動作 | 用途 |
|---|---|---|
| flushAutomatically = true | クエリ実行前に永続化コンテキストを flush | 同じトランザクション内で entity 変更と直接 UPDATE 両方使う場合 |
| clearAutomatically = true | クエリ実行後に永続化コンテキストをクリア | UPDATE 後にエンティティをロードし直したい場合 |
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Transactional
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") String status);
戻り値の意味
@Modifying 付きメソッドは戻り値で更新行数を取得できます:
@Modifying
@Transactional
@Query("UPDATE User u SET u.deleted = true WHERE u.expiredAt < :now")
int softDeleteExpired(@Param("now") LocalDateTime now);
// 呼び出し
int count = userRepository.softDeleteExpired(LocalDateTime.now());
log.info("論理削除した行数: {}", count);
JpaRepository.save() は何故問題ないか
save() や delete() など Spring Data JPA の標準メソッドは内部で自動的にトランザクションを開く設計です。手動の @Query UPDATE/DELETE だけが明示的な指定を必要とします。
| メソッド | トランザクション | @Modifying |
|---|---|---|
save() / saveAll() | 自動 | 不要 |
delete() / deleteAll() | 自動 | 不要 |
@Query SELECT ... | 不要(read-only) | 不要 |
@Query UPDATE/DELETE ... | ★ 必要 | ★ 必要 |
@Query INSERT ... (ネイティブ) | ★ 必要 | ★ 必要 |
@Transactional の付け場所のベストプラクティス
- Service クラスのメソッドに付けるのが基本(トランザクション境界 = ビジネスロジック単位)
- リポジトリの個別メソッドに付けるのは「単独で UPDATE するメソッド」だけ
- クラスレベル
@Transactionalでクラス全メソッドを対象にできる - readOnly = true を SELECT 系メソッドに付けるとパフォーマンス向上
- 例外時のロールバック: デフォルトは
RuntimeExceptionのみ。checked例外でロールバックさせるにはrollbackFor = Exception.class
関連エラー
- "Not supported for DML operations" — JPQL でなくネイティブクエリで UPDATE/DELETE する場合 →
@Query(value = "...", nativeQuery = true) - "No EntityManager with actual transaction available" — Service 層に
@Transactional付け忘れの典型 - "Cannot acquire transaction" — DataSource 設定ミス / 接続プール枯渇
- "Could not commit JPA transaction" — コミット時の制約違反 / 楽観ロック失敗など