Spring なぜMyBatisの実行したSQLがSpringの管理しているトランザクションで実行できるのか?

なぜMyBatisの実行したSQLがSpringの管理しているトランザクションで実行できるのか、を調べる。そのために、 mybatis-spring の実装を追っていく。

対象バージョン

  • java version "11.0.1" 2018-10-16 LTS
  • Spring Boot 2.1.2.RELEASE
  • mybatis-spring-boot-starter 2.0.0

はじめに

トランザクション管理は複雑で難しそうな印象があるが、結局は Spring、MyBatis ともに JDBC Driver API を利用する。そのため、以下のような処理をライブラリの中で実行しているんだ、ということを理解しておくと、読み進めやすい。

  public static void main(String[] args) throws SQLException {
    HikariDataSource ds = new HikariDataSource();
    ds.setUsername("dev");
    ds.setDriverClassName("org.postgresql.Driver");
    ds.setJdbcUrl("jdbc:postgresql://192.168.11.116:5432/dev");
    ds.setPassword("secret");

    // トランザクションの設定
    Connection conn = ds.getConnection();
    conn.setAutoCommit(false);
    conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

    // 同一コネクションを利用し、様々なSQLを実行する
    // これで同一トランザクションで処理が実行される
    PreparedStatement pstmt1 = conn.prepareStatement("INSERT INTO sample VALUES ('apple')");
    pstmt1.executeUpdate();
    PreparedStatement pstmt2 = conn.prepareStatement("INSERT INTO sample VALUES ('banana')");
    pstmt2.executeUpdate();
    
    conn.commit();
  }

処理の流れを追おう

MyBatis に詳しくない人は、あらかじめ MyBatis の概要を理解しておくと良いかもしれません。

www.kimullaa.com

ソースコード

以下を実行しながら、処理の流れを追っていく。 複雑な箇所は適宜、シーケンス図を作成する。(メインの流れではないと判断したところは適宜省略して記載するので、正確さにはやや欠けます)

@RequiredArgsConstructor
@SpringBootApplication
public class Main {

  public static void main(String[] args) {
    new SpringApplicationBuilder(Main.class)
        .web(WebApplicationType.NONE)
        .run();
  }

  private final SampleService service;

  @Bean
  public CommandLineRunner run() {
    return i -> service.insert();
  }
}
@Service
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {
  private final SampleRepository sampleRepository;

  @Override
  @Transactional
  public void insert() {
    sampleRepository.save("apple");
    sampleRepository.save("banana");
  }
}
@Mapper
public interface SampleRepository {
  @Insert("INSERT INTO sample VALUES (#{name})")
  int save(@Param("name") String name);
}

application.properties

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://192.168.11.116:5432/dev
spring.datasource.username=dev
spring.datasource.password=secret

処理の概要

  • Spring は、コネクションをスレッドローカルな値として管理する
  • MyBatis は、スレッドローカルに管理されたコネクションを取得し、同一のコネクションを利用してSQLを発行する
  • 上記2点により、 MyBatis で実行した SQL が Spring のトランザクションで実行できるようになる

Spring のトランザクション処理

Spring は、@Transactionalがついたメソッドの実行前に、 TransactionInterceptor#invoke を実行する。 この処理は、 DataSource から Connection を取得し、@Transactionalアノテーションに設定された Isolation や propagation に基づいて Connection の設定を行う。また、TransactionSynchronizationManager にスレッドローカルな値として Connection を格納する。

f:id:kimulla:20191202224246p:plain

MyBatis のメソッド実行の処理

MyBatis のMapperインタフェースを実行すると、以下の処理を実行する。

SqlSession を継承した SqlSessionTemplate というクラスは spring-mybatis が提供しており、このクラスが MyBatis の SqlSession を管理している。これによって、同一トランザクションでは同一の SqlSession インスタンスが取得できる。

f:id:kimulla:20191202224258p:plain

SqlSession の取得処理

SqlSession を取得する処理。Transaction クラスを継承した SpringManagedTransaction を返す点がポイント。

f:id:kimulla:20191202224313p:plain

クエリの実行

Executor がクエリを実行する処理。TransactionSynchronizationManager から Connection を取得する。Spring のコネクションと同様の Connection を利用して SQL を実行することで、 Spring のトランザクション上で MyBatis の処理が実行される。

f:id:kimulla:20191202224330p:plain

最後に

なるほどー。