9.

Spring Data JPA「TransactionRequiredException: Executing an

編集
この記事の要点
  • Spring Data JPA で TransactionRequiredException: Executing an update/delete query
  • 原因: 更新系クエリ(@Query で UPDATE/DELETE)にトランザクションがない
  • 対処1: リポジトリメソッドに @Modifying + @Transactional を付与
  • 対処2: Service 層のメソッドに @Transactional を付ける(推奨)
  • 読み取り専用クエリ(SELECT)には不要 — UPDATE/DELETE/INSERT のみで必須

エラー内容

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 {

    // ❌ 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 {

    @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 {
    @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" — コミット時の制約違反 / 楽観ロック失敗など
編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. java.lang.IllegalStateException: CGLIB is required to process @Configuration classes
  2. Error creating bean with name 'org.springframework.aop.config.internalAutoProxyCreator': Instantiation of bean failed; nested exception is java.lang.NoClassDefFoundError: Could not initialize class org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
  3. No mapping found for HTTP request with URI ... in DispatcherServlet with name ...
  4. An internal error occurred during: "Building UI model". com/google/common/base/Function
  5. No identifier specified for entity : ...
  6. org.hibernate.hql.internal.ast.QuerySyntaxException: table_name is not mapped
  7. No compiler is provided in this environment
  8. java.sql.SQLException: The server time zone value ' ... ' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone
  9. Caused by: java.lang.RuntimeException: Executing an update/delete query
  10. Not supported for DML operations
  11. Field ... required a bean of type ... hat could not be found.
  12. Annotation-specified bean name ' ... ' for bean class [ ... ] conflicts with existing, non-compatible bean definition of same name and class [...]
  13. Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback.
  14. Exception in thread "main" java.lang.UnsupportedClassVersionError