1. 序章

このチュートリアルでは、Javaのトランザクションの意味を理解します。 これにより、リソースのローカルトランザクションとグローバルトランザクションを実行する方法を理解できます。 これにより、JavaとSpringでトランザクションを管理するさまざまな方法を探ることもできます。

2. トランザクションとは何ですか?

一般に、Javaでのトランザクションは、すべて正常に完了する必要がある一連のアクションを指します。 したがって、 1つ以上のアクションが失敗した場合、他のすべてのアクションは、アプリケーションの状態を変更せずにバックアウトする必要があります。 これは、アプリケーションの状態の整合性が損なわれないようにするために必要です。

また、これらのトランザクションには、データベース、メッセージキューなどの1つ以上のリソースが含まれる場合があり、トランザクションの下でアクションを実行するさまざまな方法が発生します。 これには、個々のリソースを使用したリソースローカルトランザクションの実行が含まれます。 または、複数のリソースをグローバルトランザクションに参加させることもできます。

3. リソースローカルトランザクション

まず、個々のリソースを操作しながら、Javaでトランザクションを使用する方法について説明します。 ここでは、データベースなどのリソースを使用して実行する複数の個別のアクションがある場合があります。 しかし、分割できない作業単位のように、それらを統一された全体として発生させたい場合があります。 つまり、これらのアクションを1つのトランザクションで実行する必要があります。

Javaでは、データベースなどのリソースにアクセスして操作する方法がいくつかあります。 したがって、トランザクションの処理方法も同じではありません。 このセクションでは、非常に頻繁に使用されるJavaのこれらのライブラリのいくつかでトランザクションを使用する方法を説明します。

3.1. JDBC

Java Database Connectivity(JDBC)は、 JavaのAPIであり、Javaのデータベースにアクセスする方法を定義します。 さまざまなデータベースベンダーが、ベンダーに依存しない方法でデータベースに接続するためのJDBCドライバーを提供しています。 したがって、ドライバーから接続を取得して、データベースでさまざまな操作を実行します。

JDBCは、トランザクションの下でステートメントを実行するためのオプションを提供します。 接続のデフォルトの動作はauto-commitです。 明確にするために、これが意味するのは、すべてのステートメントがトランザクションとして扱われ、実行直後に自動的にコミットされるということです。

ただし、1つのトランザクションに複数のステートメントをバンドルする場合は、次のことも実行できます。

Connection connection = DriverManager.getConnection(CONNECTION_URL, USER, PASSWORD);
try {
    connection.setAutoCommit(false);
    PreparedStatement firstStatement = connection .prepareStatement("firstQuery");
    firstStatement.executeUpdate();
    PreparedStatement secondStatement = connection .prepareStatement("secondQuery");
    secondStatement.executeUpdate();
    connection.commit();
} catch (Exception e) {
    connection.rollback();
}

ここでは、接続の自動コミットモードを無効にしました。 したがって、手動でトランザクション境界を定義し、コミットまたはロールバックを実行できます。 JDBCを使用すると、セーブポイントを設定して、ロールバックする量をより細かく制御することもできます。

3.2. JPA

Java Persistence API(JPA)は、オブジェクト指向ドメインモデルとリレーショナルデータベースシステムの間のギャップを埋めるために使用できるJavaの仕様です。 そのため、Hibernate、EclipseLink、iBatisなどのサードパーティから入手可能なJPAの実装がいくつかあります。

JPAでは、通常のクラスを、永続的なIDを提供するエンティティとして定義できます。 EntityManagerクラスは、永続コンテキスト内で複数のエンティティを操作するために必要なインターフェイスを提供します。 永続性コンテキストは、エンティティが管理される第1レベルのキャッシュと考えることができます。

JPAアーキテクチャ

ここでの永続コンテキストには、トランザクションスコープまたは拡張スコープの2つのタイプがあります。 トランザクションスコープの永続コンテキストは、単一のトランザクションにバインドされます。 拡張スコープの永続コンテキストは複数のトランザクションにまたがることができます。 永続コンテキストのデフォルトスコープはtransaction-scopeです。

EntityManager を作成し、トランザクション境界を手動で定義する方法を見てみましょう。

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-example");
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
    entityManager.getTransaction().begin();
    entityManager.persist(firstEntity);
    entityManager.persist(secondEntity);
    entityManager.getTransaction().commit();
} catch (Exception e) {
    entityManager.getTransaction().rollback();
}

ここでは、トランザクションスコープの永続コンテキスト内でEntityManagerFactoryからEntityManagerを作成しています。 次に、 begin commit、、およびrollbackメソッドを使用してトランザクション境界を定義します。

3.3. JMS

Javaメッセージングサービス(JMS)は、アプリケーションがメッセージを使用して非同期で通信できるようにするJavaの仕様です。 APIを使用すると、キューまたはトピックからメッセージを作成、送信、受信、および読み取ることができます。 OpenMQやActiveMQなど、JMS仕様に準拠するメッセージングサービスがいくつかあります。

JMS APIは、単一のトランザクションで複数の送信または受信操作をバンドルすることをサポートします。 ただし、メッセージベースの統合アーキテクチャの性質上、メッセージの生成と消費を同じトランザクションの一部にすることはできません。 トランザクションのスコープは、クライアントとJMSプロバイダーの間で維持されます。

JMSを使用すると、ベンダー固有のConnectionFactoryから取得した接続からセッションを作成できます。 トランザクションされるかどうかのセッションを作成するためのオプションがあります。 非トランザクションSession s の場合、適切な確認応答モードをさらに定義することもできます。

トランザクションセッションを作成して、トランザクションの下で複数のメッセージを送信する方法を見てみましょう。

ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(CONNECTION_URL);
Connection connection = = connectionFactory.createConnection();
connection.start();
try {
    Session session = connection.createSession(true, 0);
    Destination = destination = session.createTopic("TEST.FOO");
    MessageProducer producer = session.createProducer(destination);
    producer.send(firstMessage);
    producer.send(secondMessage);
    session.commit();
} catch (Exception e) {
    session.rollback();
}

ここでは、トピックのタイプの宛先に対してMessageProducerを作成しています。 前に作成したセッションから宛先を取得します。 さらに、セッションを使用して、メソッドcommitおよびrollbackを使用してトランザクション境界を定義します。

4. グローバルトランザクション

リソースのローカルトランザクションを見たように、単一のリソース内で複数の操作を統合された全体として実行できます。 ただし、複数のリソースにまたがる操作を扱うことがよくあります。 たとえば、2つの異なるデータベース、またはデータベースとメッセージキューでの操作。 ここでは、リソース内のローカルトランザクションサポートでは不十分です。

これらのシナリオで必要なのは、複数の参加リソースにまたがるトランザクションを区別するためのグローバルメカニズムです。 これはしばしば分散トランザクションとして知られており、それらを効果的に処理するために提案された仕様があります。

XA仕様は、複数のリソースにまたがるトランザクションを制御するトランザクションマネージャーを定義するそのような仕様の1つです。 Javaは、コンポーネントJTAおよびJTSを介してXA仕様に準拠する分散トランザクションをかなり成熟してサポートしています。

4.1. JTA

Java Transaction API(JTA)は、JavaCommunityProcessの下で開発されたJavaEnterpriseEditionAPIです。 JavaアプリケーションとアプリケーションサーバーがXAリソース全体で分散トランザクションを実行できるようにします。 JTAは、2フェーズコミットを活用してXAアーキテクチャをモデルにしています。

JTAは、トランザクションマネージャと分散トランザクションの他の関係者との間の標準Javaインターフェイスを指定します。

上で強調表示されている主要なインターフェースのいくつかを理解しましょう。

  • TransactionManager:アプリケーションサーバーがトランザクションの境界を定めて制御できるようにするインターフェイス
  • UserTransaction:このインターフェースにより、アプリケーションプログラムはトランザクションを明示的に区別および制御できます。
  • XAResource このインターフェースの目的は、トランザクションマネージャーがXA準拠のリソースのリソースマネージャーと連携できるようにすることです。

4.2. JTS

Java Transaction Service(JTS)は、OMGOTS仕様にマップするトランザクションマネージャーを構築するための仕様です。 JTSは、標準のCORBA ORB / TSインターフェイスとインターネットInter-ORBプロトコル(IIOP)を使用して、JTSトランザクションマネージャー間のトランザクションコンテキストの伝播を行います。

高レベルでは、Java Transaction API(JTA)をサポートします。 JTSトランザクションマネージャーは、分散トランザクションに関係する当事者にトランザクションサービスを提供します。

JTSがアプリケーションに提供するサービスはほとんど透過的であるため、アプリケーションアーキテクチャでそれらに気付かない場合もあります。 JTSは、アプリケーションプログラムからすべてのトランザクションセマンティクスを抽象化するアプリケーションサーバーを中心に設計されています。

5. JTAトランザクション管理

次に、JTAを使用して分散トランザクションを管理する方法を理解します。 分散トランザクションは簡単なソリューションではないため、コストにも影響します。 さらに、アプリケーションにJTAを含めるために選択できるオプションが複数あります。 したがって、私たちの選択は、全体的なアプリケーションアーキテクチャと願望の観点から行う必要があります。

5.1. アプリケーションサーバーのJTA

前に見たように、JTAアーキテクチャはアプリケーションサーバーに依存して、トランザクション関連の多くの操作を容易にします。 サーバーが提供するためにサーバーに依存している主要なサービスの1つは、JNDIを介したネーミングサービスです。 これは、データソースなどのXAリソースがバインドおよび取得される場所です。

これとは別に、アプリケーションでトランザクション境界をどのように管理するかという点で選択肢があります。 これにより、Javaアプリケーションサーバー内で2種類のトランザクションが発生します。

  • コンテナ管理トランザクション:名前が示すように、ここでトランザクション境界はアプリケーションサーバーによって設定されます。 これにより、Enterprise Java Beans(EJB)の開発が簡素化されます。これは、トランザクションの境界設定に関連するステートメントが含まれておらず、コンテナーのみに依存しているためです。 ただし、これではアプリケーションに十分な柔軟性が提供されません。
  • Bean管理トランザクション:コンテナー管理トランザクションとは異なり、Bean管理トランザクションでは EJBには、トランザクション境界を定義する明示的なステートメントが含まれています。 これにより、複雑さが増しますが、トランザクションの境界をマークする際にアプリケーションを正確に制御できます。

アプリケーションサーバーのコンテキストでトランザクションを実行することの主な欠点の1つは、アプリケーションがサーバーと緊密に結合されることです。 これは、アプリケーションの妥当性、管理性、および移植性に関して影響を及ぼします。 これは、サーバーに依存しないアプリケーションの開発に重点が置かれているマイクロサービスアーキテクチャではより深刻です。

5.2. JTAスタンドアロン

前のセクションで説明した問題は、アプリケーションサーバーに依存しない分散トランザクション用のソリューションの作成に向けて大きな勢いをもたらしました。 この点に関して、Springでトランザクションサポートを使用したり、Atomikosなどのトランザクションマネージャーを使用したりするなど、いくつかのオプションを利用できます。

Atomikos などのトランザクションマネージャーを使用して、データベースとメッセージキューを使用した分散トランザクションを容易にする方法を見てみましょう。 分散トランザクションの重要な側面の1つは、トランザクションモニターを使用して参加リソースを登録および除外することです。 Atomikosがこれを処理してくれます。 私たちがしなければならないのは、Atomikosが提供する抽象化を使用することだけです。

AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
DataSource dataSource = atomikosDataSourceBean;

ここでは、 AtomikosDataSourceBean のインスタンスを作成し、ベンダー固有のXADataSourceを登録しています。 これからは、他の DataSource と同じようにこれを使い続け、分散トランザクションのメリットを享受できます。

同様に、メッセージキューの抽象化があり、ベンダー固有のXAリソースをトランザクションモニターに自動的に登録します。

AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = new AtomikosConnectionFactoryBean();
atomikosConnectionFactoryBean.setXaConnectionFactory(new ActiveMQXAConnectionFactory());
ConnectionFactory connectionFactory = atomikosConnectionFactoryBean;

ここでは、 AtomikosConnectionFactoryBean のインスタンスを作成し、XA対応のJMSベンダーからXAConnectionFactoryを登録しています。 この後、これを通常のConnectionFactoryとして引き続き使用できます。

さて、Atomikosは、すべてをまとめるためのパズルの最後のピース、UserTransactionのインスタンスを提供します。

UserTransaction userTransaction = new UserTransactionImp();

これで、データベースとメッセージキューにまたがる分散トランザクションを使用してアプリケーションを作成する準備が整いました。

try {
    userTransaction.begin();

    java.sql.Connection dbConnection = dataSource.getConnection();
    PreparedStatement preparedStatement = dbConnection.prepareStatement(SQL_INSERT);
    preparedStatement.executeUpdate();

    javax.jms.Connection mbConnection = connectionFactory.createConnection();
    Session session = mbConnection.createSession(true, 0);
    Destination destination = session.createTopic("TEST.FOO");
    MessageProducer producer = session.createProducer(destination);
    producer.send(MESSAGE);

    userTransaction.commit();
} catch (Exception e) {
    userTransaction.rollback();
}

ここでは、メソッドbeginとcommitをクラスUserTransactionで使用して、トランザクション境界の境界を定めています。 これには、データベースへのレコードの保存と、メッセージキューへのメッセージの公開が含まれます。

6. 春のトランザクションサポート

トランザクションの処理は、多くの定型コーディングと構成を含むかなり複雑なタスクであることがわかりました。 さらに、各リソースには、ローカルトランザクションを処理する独自の方法があります。 Javaでは、JTAはこれらのバリエーションから私たちを抽象化しますが、プロバイダー固有の詳細とアプリケーションサーバーの複雑さをさらにもたらします。

Springプラットフォームは、Javaでのリソースローカルトランザクションとグローバルトランザクションの両方のトランザクションを処理するためのはるかにクリーンな方法を提供します。 これは、Springの他の利点とともに、Springを使用してトランザクションを処理するための説得力のあるケースを作成します。 さらに、Springを使用してトランザクションマネージャーを構成および切り替えるのは非常に簡単です。Springはサーバー提供またはスタンドアロンにすることができます。

Springは、トランザクションコードを使用してメソッドのプロキシを作成することにより、このシームレスな抽象化を提供します。 プロキシは、 TransactionManager を使用して、コードに代わってトランザクション状態を管理します。ここでの中心的なインターフェイスは、 PlatformTransactionManager であり、さまざまな実装を利用できます。 JDBC(DataSource)、JMS、JPA、JTA、およびその他の多くのリソースを介した抽象化を提供します。

6.1. 構成

Atomikosをトランザクションマネージャーとして使用し、JPAとJMSのトランザクションサポートを提供するようにSpringを構成する方法を見てみましょう。 まず、タイプJTAのPlatformTransactionManagerを定義します。

@Bean
public PlatformTransactionManager platformTransactionManager() throws Throwable {
    return new JtaTransactionManager(
                userTransaction(), transactionManager());
}

ここでは、UserTransactionおよびTransactionManagerからJTATransactionManagerのインスタンスを提供しています。 これらのインスタンスは、Atomikosのようなトランザクションマネージャーライブラリによって提供されます。

@Bean
public UserTransaction userTransaction() {
    return new UserTransactionImp();
}

@Bean(initMethod = "init", destroyMethod = "close")
public TransactionManager transactionManager() {
    return new UserTransactionManager();
}

クラスUserTransactionImpおよびUserTransactionManagerは、Atomikosによってここで提供されます。

さらに、Springで同期JMSアクセスを許可するコアクラスであるJmsTempleteを定義する必要があります。

@Bean
public JmsTemplate jmsTemplate() throws Throwable {
    return new JmsTemplate(connectionFactory());
}

ここで、 ConnectionFactory はAtomikosによって提供され、Connectionによって提供される分散トランザクションを有効にします。

@Bean(initMethod = "init", destroyMethod = "close")
public ConnectionFactory connectionFactory() {
    ActiveMQXAConnectionFactory activeMQXAConnectionFactory = new 
ActiveMQXAConnectionFactory();
    activeMQXAConnectionFactory.setBrokerURL("tcp://localhost:61616");
    AtomikosConnectionFactoryBean atomikosConnectionFactoryBean = new AtomikosConnectionFactoryBean();
    atomikosConnectionFactoryBean.setUniqueResourceName("xamq");
    atomikosConnectionFactoryBean.setLocalTransactionMode(false);
atomikosConnectionFactoryBean.setXaConnectionFactory(activeMQXAConnectionFactory);
    return atomikosConnectionFactoryBean;
}

したがって、ご覧のとおり、ここでは、JMSプロバイダー固有のXAConnectionFactoryAtomikosConnectionFactoryBeanでラップしています。

次に、SpringでJPA EntityManagerFactoryBeanの作成を担当するAbstractEntityManagerFactoryBeanを定義する必要があります。

@Bean
public LocalContainerEntityManagerFactoryBean entityManager() throws SQLException {
    LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
    entityManager.setDataSource(dataSource());
    Properties properties = new Properties();
    properties.setProperty( "javax.persistence.transactionType", "jta");
    entityManager.setJpaProperties(properties);
    return entityManager;
}

以前と同様に、LocalContainerEntityManagerFactoryBeanで設定したDataSourceは、分散トランザクションが有効になっているAtomikosによって提供されます。

@Bean(initMethod = "init", destroyMethod = "close")
public DataSource dataSource() throws SQLException {
    MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
    mysqlXaDataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test");
    AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
    xaDataSource.setXaDataSource(mysqlXaDataSource);
    xaDataSource.setUniqueResourceName("xads");
    return xaDataSource;
}

ここでも、プロバイダー固有のXADataSourceAtomikosDataSourceBeanでラップしています。

6.2. トランザクション管理

前のセクションのすべての構成を完了したので、私たちはかなり圧倒されていると感じなければなりません! 結局のところ、Springを使用することの利点に疑問を呈することさえあるかもしれません。 ただし、このすべての構成により、ほとんどのプロバイダー固有のボイラープレートからの抽象化が可能になり、実際のアプリケーションコードでそれを認識する必要がないことを忘れないでください。

これで、データベースを更新してメッセージを公開する予定のSpringでトランザクションを使用する方法を検討する準備が整いました。 Springは、これを実現するための2つの方法を提供し、独自の利点から選択できます。 それらをどのように利用できるかを理解しましょう。

  • 宣言型サポート

Springでトランザクションを使用する最も簡単な方法は、宣言型のサポートを使用することです。 ここでは、メソッドまたはクラスでさえ適用できる便利なアノテーションがあります。 これにより、コードのグローバルトランザクションが有効になります。

@PersistenceContext
EntityManager entityManager;

@Autowired
JmsTemplate jmsTemplate;

@Transactional(propagation = Propagation.REQUIRED)
public void process(ENTITY, MESSAGE) {
   entityManager.persist(ENTITY);
   jmsTemplate.convertAndSend(DESTINATION, MESSAGE);
}

上記の単純なコードは、データベースでの保存操作とJTAトランザクション内のメッセージキューでの公開操作を可能にするのに十分です。

  • プログラマティックサポート

宣言型サポートは非常にエレガントでシンプルですが、トランザクション境界をより正確に制御する利点を提供していません。 したがって、それを達成するための特定の必要性がある場合、Springはトランザクション境界を区切るためのプログラムによるサポートを提供します。

@Autowired
private PlatformTransactionManager transactionManager;

public void process(ENTITY, MESSAGE) {
    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
    transactionTemplate.executeWithoutResult(status -> {
        entityManager.persist(ENTITY);
        jmsTemplate.convertAndSend(DESTINATION, MESSAGE);
    });
}

したがって、ご覧のとおり、使用可能なPlatformTransactionManagerを使用してTransactionTemplateを作成する必要があります。 次に、 TransactionTemplete を使用して、グローバルトランザクション内の一連のステートメントを処理できます。

7. 後付け

これまで見てきたように、トランザクション、特に複数のリソースにまたがるトランザクションの処理は複雑です。 さらに、トランザクションは本質的にブロッキングであり、アプリケーションの遅延とスループットに悪影響を及ぼします。 さらに、分散トランザクションを使用したコードのテストと保守は、特にトランザクションが基盤となるアプリケーションサーバーに依存している場合は簡単ではありません。 したがって、全体として、可能であればトランザクションをまったく回避するのが最善です。

しかし、それは現実にはほど遠いです。 つまり、実際のアプリケーションでは、トランザクションが正当に必要になることがよくあります。 トランザクションなしでアプリケーションアーキテクチャを再考することは可能ですが、常に可能であるとは限りません。 したがって、Javaでトランザクションを操作するときは、アプリケーションを改善するために特定のベストプラクティスを採用する必要があります。

  • 採用すべき基本的なシフトの1つは、アプリケーションサーバーによって提供されるものではなく、スタンドアロンのトランザクションマネージャーを使用することです。 これだけで、アプリケーションを大幅に簡素化できます。 さらに、クラウドネイティブのマイクロサービスアーキテクチャに非常に適しています。
  • さらに、 Springのような抽象化レイヤーは、JPAやJTAプロバイダーのようなプロバイダーの直接的な影響を抑えるのに役立ちます。 したがって、これにより、ビジネスロジックに大きな影響を与えることなく、プロバイダーを切り替えることができます。 さらに、トランザクション状態を管理するという低レベルの責任を私たちから取り除きます。
  • 最後に、コードでトランザクション境界を選択する際に注意する必要があります。 トランザクションはブロックされているため、トランザクションの境界を可能な限り制限することをお勧めします。 必要に応じて、トランザクションの宣言型制御よりもプログラムによる制御を優先する必要があります。

8. 結論

要約すると、このチュートリアルでは、Javaのコンテキストでのトランザクションについて説明しました。 さまざまなリソースについて、Javaでの個々のリソースのローカルトランザクションのサポートを行いました。 また、Javaでグローバルトランザクションを実現する方法についても説明しました。

さらに、Javaでグローバルトランザクションを管理するためのさまざまな方法を試しました。 また、SpringによってJavaでのトランザクションの使用がどのように簡単になるかを理解しました。

最後に、Javaでトランザクションを操作する際のいくつかのベストプラクティスを確認しました。