親バカエンジニアのナレッジ帳

webのエンジニアをやっており、日頃の開発で詰まったことや書き残しておきたいことを載せています。

Spring Bootのトランザクションが効かない場合のチェック項目


Spring Bootでのトランザクション設定方法

Spring Bootでトランザクションを設定する方法としては、TransactionManagerを使う方法と@Transactionalというアノテーションを使う方法があります。
コード量の関係やメンテナンス性などを考慮しても後者を選んで設定している人が多いのではないでしょうか。

ただ、設定したはいいものの上手く効かないという人もいると思います。
今回は後者を選んで設定した場合で、トランザクションが上手くいかない場合のチェック項目を記します。
以下に当てはまる場合はトランザクションが効かないので注意が必要です。

メソッドがpublicになっているか

@Transactionalはpublicのメソッドやクラスにしか効きません。(理由は後述)
メソッドに設定した場合に関してですが、Spring Bootではにpublicやprivateなどの修飾子をつけない場合、デフォルトではpublicの扱いになります。
なので、publicをつけなくてもpublicの扱いになるから大丈夫だろうと思われる人もいるかもしれませんが、ちゃんとpublicをつけないとトランザクションが効かないのです。

ダメな例)

@Service
public class TestService {

  @Transactional
  void testMethod() {
    // 更新処理
  }

}


正しい例)

@Service
public class TestService {

  @Transactional
  public void testMethod() {
    // 更新処理
  }

}


@RequestMappingをつけているコントローラ自体にアノテーションを付与していないか

Spring Bootではパスによってコントローラのメソッドを呼び出す場合、@RequestMappingが付与されますが、そのようなメソッド自体に@Transactionalを付与してもトランザクションは効きません。
@AutowiredでDIしたクラスのメソッドを直接呼び出す形でないといけません。

ダメな例)

@Controller
public class TestController {

  @RequestMapping(value = "test", method = RequestMethod.POST)
  @Transactional
  ResponseEntity test() {
    // 更新処理
    return "ok";
  }
}


正しい例)

@Controller
public class TestController {

  @Autowired
  TestService testService;

  @RequestMapping(value = "test", method = RequestMethod.POST)
  ResponseEntity test() {

    try {
      testService.testMethod(); // このメソッド内で更新処理がされる
    } catch (Exception e) {
      e.printStackTrace();
    }

    return "ok";
  }
}
@Service
public class TestService {

  @Transactional
  public void testMethod() {
    // 更新処理
  }

}


try~catchで囲まれているか

上記の正しい例にある通り、トランザクションを使用した更新処理はtry~catchで囲む必要があります。
トランザクションでは、エラーが発生した場合に更新されたデータが全て元に戻る(ロールバック)のですが、この時にExceptionの例外でないとロールバックはされないのです。


ダメな例)

@Controller
public class TestController {

  @Autowired
  TestService testService;

  @RequestMapping(value = "test", method = RequestMethod.POST)
  ResponseEntity test() {

    testService.testMethod(); // このメソッド内で更新処理がされる

    return "ok";
  }
}
@Service
public class TestService {

  @Transactional
  public void testMethod() {
    // 更新処理
  }

}


正しい例)

@Controller
public class TestController {

  @Autowired
  TestService testService;

  @RequestMapping(value = "test", method = RequestMethod.POST)
  ResponseEntity test() {

    try {
      testService.testMethod(); // このメソッド内で更新処理がされる
    } catch (Exception e) {
      e.printStackTrace();
    }

    return "ok";
  }
}
@Service
public class TestService {

  @Transactional
  public void testMethod() {
    // 更新処理
  }

}


更新処理をするメソッドが直接呼ばれているか

これはクラス全体ではなくメソッドにアノテーションを付与した場合に関係することですが、コントローラなどから別メソッドを呼び出して更新処理をする場合、@AutowiredでDIしたクラスのメソッドを直接呼び出す形でないとトランザクションは効きません。
例えば以下の例では上手くいきません。

ダメな例)

@Controller
public class TestController {

  @Autowired
  TestService testService;

  @RequestMapping(value = "test", method = RequestMethod.POST)
  ResponseEntity test() {

    try {
      testService.testMethod(); // このメソッド内で更新処理がされる
    } catch (Exception e) {
      e.printStackTrace();
    }

    return "ok";
  }
}
@Service
public class TestService {

  public void testMethod() {
    testInsertMethod()
  }

  @Transactional
  public void testInsertMethod() {
    // 更新処理
  }

}

上記の例では、一度testMethod()というメソッドを経由してから更新処理を行うメソッドtestInsertMethod()を実行していますよね。
これではDIしたクラスのメソッドを直接呼び出しているわけではないので、トランザクションはかかりません。
以下のように直接呼び出しましょう。

正しい例)

@Controller
public class TestController {

  @Autowired
  TestService testService;

  @RequestMapping(value = "test", method = RequestMethod.POST)
  ResponseEntity test() {

    try {
      testService.testMethod(); // このメソッド内で更新処理がされる
    } catch (Exception e) {
      e.printStackTrace();
    }

    return "ok";
  }
}
@Service
public class TestService {

  @Transactional
  public void testMethod() {
    // 更新処理
  }

}

先ほど@Transactionalはpublicのメソッドやクラスにしか効きないと記載しましたが、メソッドではコントローラなどから直接呼び出す必要がある以上、いずれにせよprivateにはできないのです。


継承されたメソッドの場合は子クラスに@Transactionalを付与

トランザクションを設定したいメソッドが親クラスの抽象メソッドなどを継承して実装するパターンの場合、抽象メソッドに@Transactionalが付いていても子クラスで実装するメソッドにも@Transactionalを付与しないとトランザクションは効きません。

通常、継承されたメソッドはアノテーションも継承されるものです。
しかしトランザクションの場合は、親メソッドにアノテーションを付与するだけではやはり直接呼び出していないと見なされ、トランザクションは上手く効きません。

今回の例はServiceクラスのみ書きます。

ダメな例)

@Service
abstract public class TestBaseService {

  @Transactional
  abstract public void testMethod()

}

@Service
public class TestService extends TestBaseService {

  @Override
  public void testMethod() {
    // 更新処理
  }

}

上記の例では、testMethodは抽象メソッドの実装になりますが、testMethod()自身にも@Transactionalを付与していないのでトランザクションは効きません。


正しい例)

@Service
abstract public class TestBaseService {

  @Transactional
  abstract public void testMethod()

}

@Service
public class TestService extends TestBaseService {

  @Override
  @Transactional
  public void testMethod() {
    // 更新処理
  }

}

上記のように実装メソッドにも@Transactionalを付与する必要があることに注意しましょう。

まとめ

ここまでトランザションが効かない場合の主なチェック項目を挙げましたが、上記を満たせばほとんどの場合トランザクションが効くようになるはずです。


Spring Boot 2 プログラミング入門

Spring Boot 2 プログラミング入門