@Transactional アノテーションでトランザクション管理

このガイドでは、データベース操作を非侵入型トランザクションでラップするプロセスについて説明します。

構築するもの

特別な JDBC コードを記述する [Oracle] (英語) ことなく、データベース操作をトランザクション対応にする単純な JDBC アプリケーションを構築します。

必要なもの

本ガイドの完成までの流れ

ほとんどの Spring 入門ガイドと同様に、最初から始めて各ステップを完了するか、すでに慣れている場合は基本的なセットアップステップをバイパスできます。いずれにしても、最終的に動作するコードになります。

最初から始めるには、Spring Initializr から開始に進みます。

基本をスキップするには、次の手順を実行します。

完了したときは、gs-managing-transactions/complete のコードに対して結果を確認できます。

Spring Initializr から開始

IDE を使用する場合はプロジェクト作成ウィザードを使用します。IDE を使用せずにコマンドラインなどで開発する場合は、この事前に初期化されたプロジェクトからプロジェクトを ZIP ファイルとしてダウンロードできます。このプロジェクトは、このチュートリアルの例に合うように構成されています。

プロジェクトを手動で初期化するには:

  1. IDE のメニューまたはブラウザーから Spring Initializr を開きます。アプリケーションに必要なすべての依存関係を取り込み、ほとんどのセットアップを行います。

  2. Gradle または Maven のいずれかと、使用する言語を選択してください。

  3. 「アーティファクト」フォームフィールドに "managing-transactions" と入力します。

  4. 依存関係をクリックし、Spring Data JDBC と H2 Database を選択します。

  5. 生成をクリックします。

  6. 結果の ZIP ファイルをダウンロードします。これは、選択して構成された Web アプリケーションのアーカイブです。

Eclipse や IntelliJ のような IDE は新規プロジェクト作成ウィザードから Spring Initializr の機能が使用できるため、手動での ZIP ファイルのダウンロードやインポートは不要です。
プロジェクトを GitHub からフォークして、IDE または他のエディターで開くこともできます。

予約サービスを作成する

まず、BookingService クラスを使用して、名前でシステムへの予約を行う JDBC ベースのサービスを作成する必要があります。以下のリストは、その方法を示しています。

Java
package com.example.managingtransactions;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class BookingService {

	private final static Logger logger = LoggerFactory.getLogger(BookingService.class);

	private final JdbcTemplate jdbcTemplate;

	public BookingService(JdbcTemplate jdbcTemplate) {
		this.jdbcTemplate = jdbcTemplate;
	}

	@Transactional
	public void book(String... persons) {
		for (String person : persons) {
			logger.info("Booking {} in a seat...", person);
			jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person);
		}
	}

	public List<String> findAllBookings() {
		return jdbcTemplate.query("select FIRST_NAME from BOOKINGS",
				(rs, rowNum) -> rs.getString("FIRST_NAME"));
	}

}
Kotlin
package com.example.managingtransactions

import org.slf4j.LoggerFactory
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

private val logger = LoggerFactory.getLogger(BookingService::class.java)

@Component
class BookingService(private val jdbcTemplate: JdbcTemplate) {

    @Transactional
    fun book(vararg persons: String?) {
        persons.forEach { person ->
            logger.info("Booking {} in a seat...", person)
            jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person)
        }
    }

    fun findAllBookings(): List<String> =
        jdbcTemplate.query("select FIRST_NAME from BOOKINGS") { rs, _ ->
            rs.getString("FIRST_NAME")
        }
}

コードにはオートワイヤーされた JdbcTemplate があります。これは、残りのコードで必要なすべてのデータベース相互作用を行う便利なテンプレートクラスです。

複数の人を予約できる book メソッドもあります。人物のリストをループし、各人物について、JdbcTemplate を使用してその人物を BOOKINGS テーブルに挿入します。このメソッドには @Transactional のタグが付けられています。つまり、障害が発生すると、操作全体が以前の状態にロールバックされ、元の例外が再スローされます。これは、1 人が追加に失敗した場合、BOOKINGS に追加される人はいないことを意味します。

データベースを照会する findAllBookings メソッドもあります。データベースからフェッチされた各行は String に変換され、すべての行は List にアセンブルされます。

アプリケーションを構築する

Spring Initializr はアプリケーションクラスを提供します。この場合、このアプリケーションクラスを変更する必要はありません。次のリストはアプリケーションクラスを示しています。

Java
package com.example.managingtransactions;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ManagingTransactionsApplication {

	public static void main(String[] args) {
		SpringApplication.run(ManagingTransactionsApplication.class, args);
	}

}
Kotlin
package com.example.managingtransactions

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class ManagingTransactionsApplication

fun main(args: Array<String>) {
    runApplication<ManagingTransactionsApplication>(*args)
}

@SpringBootApplication は、次のすべてを追加する便利なアノテーションです。

  • @Configuration: アプリケーションコンテキストの Bean 定義のソースとしてクラスにタグを付けます。

  • @EnableAutoConfiguration: クラスパス設定、他の Bean、さまざまなプロパティ設定に基づいて Bean の追加を開始するよう Spring Boot に指示します。例: spring-webmvc がクラスパスにある場合、このアノテーションはアプリケーションに Web アプリケーションとしてフラグを立て、DispatcherServlet のセットアップなどの主要な動作をアクティブにします。

  • @ComponentScan: Spring に、com/example パッケージ内の他のコンポーネント、構成、サービスを探して、コントローラーを検出させるように指示します。

main() メソッドは、Spring Boot の SpringApplication.run() メソッドを使用してアプリケーションを起動します。XML が 1 行もないことに気付きましたか? web.xml ファイルもありません。この Web アプリケーションは 100% 純粋な Java であり、接続機能やインフラストラクチャの構成に対処する必要はありませんでした。

アプリケーションの構成は実際にはゼロです。Spring Boot は、クラスパスで spring-jdbc および h2 を検出し、DataSource および JdbcTemplate を自動的に作成します。このインフラストラクチャが利用可能になり、専用の構成がないため、DataSourceTransactionManager も作成されます。これは、@Transactional アノテーションが付けられたメソッド(たとえば、BookingService の book メソッド)をインターセプトするコンポーネントです。BookingService は、クラスパススキャンによって検出されます。

このガイドで示されている別の Spring Boot 機能は、起動時にスキーマを初期化する機能です。次のファイル(src/main/resources/schema.sql から)は、データベーススキーマを定義しています。

drop table BOOKINGS if exists;
create table BOOKINGS(ID serial, FIRST_NAME varchar(5) NOT NULL);

BookingService を注入し、様々なトランザクションユースケースを実証する CommandLineRunner もあります。以下のリストはコマンドラインランナーを示しています。

Java
package com.example.managingtransactions;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

@Component
class AppRunner implements CommandLineRunner {

	private final static Logger logger = LoggerFactory.getLogger(AppRunner.class);

	private final BookingService bookingService;

	public AppRunner(BookingService bookingService) {
		this.bookingService = bookingService;
	}

	@Override
	public void run(String... args) throws Exception {
		bookingService.book("Alice", "Bob", "Carol");
		Assert.isTrue(bookingService.findAllBookings().size() == 3,
				"First booking should work with no problem");
		logger.info("Alice, Bob and Carol have been booked");

		try {
			bookingService.book("Chris", "Samuel");
		} catch (RuntimeException e) {
			logger.info("v--- The following exception is expect because 'Samuel' is too " +
					"big for the DB ---v");
			logger.error(e.getMessage());
		}

		logCurrentBookings();
		logger.info("You shouldn't see Chris or Samuel. Samuel violated DB constraints, " +
				"and Chris was rolled back in the same TX");
		Assert.isTrue(bookingService.findAllBookings().size() == 3,
				"'Samuel' should have triggered a rollback");

		try {
			bookingService.book("Buddy", null);
		} catch (RuntimeException e) {
			logger.info("v--- The following exception is expect because null is not " +
					"valid for the DB ---v");
			logger.error(e.getMessage());
		}

		logCurrentBookings();
		logger.info("You shouldn't see Buddy or null. null violated DB constraints, and " +
				"Buddy was rolled back in the same TX");
		Assert.isTrue(bookingService.findAllBookings().size() == 3,
				"'null' should have triggered a rollback");
	}

	private void logCurrentBookings() {
		for (String person : bookingService.findAllBookings()) {
            logger.info("So far, {} is booked.", person);
		}
	}
}
Kotlin
package com.example.managingtransactions

import org.slf4j.LoggerFactory
import org.springframework.boot.CommandLineRunner
import org.springframework.stereotype.Component
import org.springframework.util.Assert

private val logger = LoggerFactory.getLogger(AppRunner::class.java)

@Component
class AppRunner(private val bookingService: BookingService) : CommandLineRunner {

    override fun run(vararg args: String) {
        bookingService.book("Alice", "Bob", "Carol")
        Assert.isTrue(bookingService.findAllBookings().size == 3,
            "First booking should work with no problem")
        logger.info("Alice, Bob and Carol have been booked")

        try {
            bookingService.book("Chris", "Samuel")
        } catch (e: RuntimeException) {
            logger.info("v--- The following exception is expect because 'Samuel' is too big for the DB ---v")
            logger.error(e.message)
        }

        logCurrentBookings()
        logger.info("You shouldn't see Chris or Samuel. Samuel violated DB constraints, and Chris was rolled back in the same TX")
        Assert.isTrue(bookingService.findAllBookings().size == 3,
            "'Samuel' should have triggered a rollback")

        try {
            bookingService.book("Buddy", null)
        } catch (e: RuntimeException) {
            logger.info("v--- The following exception is expect because null is not valid for the DB ---v")
            logger.error(e.message)
        }

        logCurrentBookings()
        logger.info("You shouldn't see Buddy or null. null violated DB constraints, and Buddy was rolled back in the same TX")
        Assert.isTrue(bookingService.findAllBookings().size == 3,
            "'null' should have triggered a rollback")
    }

    private fun logCurrentBookings() {
        bookingService.findAllBookings().forEach { person ->
            logger.info("So far, {} is booked.", person)
        }
    }
}

コマンドラインから Gradle または Maven を使用してアプリケーションを実行できます。必要なすべての依存関係、クラス、リソースを含む単一の実行可能 JAR ファイルを構築して実行することもできます。実行可能な jar を構築すると、開発ライフサイクル全体、さまざまな環境などで、アプリケーションとしてサービスを簡単に提供、バージョン管理、デプロイできます。

Gradle を使用する場合、./gradlew bootRun を使用してアプリケーションを実行できます。または、次のように、./gradlew build を使用して JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar build/libs/gs-managing-transactions-0.0.1-SNAPSHOT.jar

Maven を使用する場合、./mvnw spring-boot:run を使用してアプリケーションを実行できます。または、次のように、./mvnw clean package で JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar target/gs-managing-transactions-0.0.1-SNAPSHOT.jar

次の出力が表示されます。

2019-09-19 14:05:25.111  INFO 51911 --- [           main] c.e.m.ManagingTransactionsApplication    : Starting ManagingTransactionsApplication on Jays-MBP with PID 51911 (/Users/j/projects/guides/gs-managing-transactions/complete/target/classes started by j in /Users/j/projects/guides/gs-managing-transactions/complete)
2019-09-19 14:05:25.114  INFO 51911 --- [           main] c.e.m.ManagingTransactionsApplication    : No active profile set, falling back to default profiles: default
2019-09-19 14:05:25.421  INFO 51911 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2019-09-19 14:05:25.438  INFO 51911 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 13ms. Found 0 repository interfaces.
2019-09-19 14:05:25.678  INFO 51911 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-09-19 14:05:25.833  INFO 51911 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-09-19 14:05:26.158  INFO 51911 --- [           main] c.e.m.ManagingTransactionsApplication    : Started ManagingTransactionsApplication in 1.303 seconds (JVM running for 3.544)
2019-09-19 14:05:26.170  INFO 51911 --- [           main] c.e.managingtransactions.BookingService  : Booking Alice in a seat...
2019-09-19 14:05:26.181  INFO 51911 --- [           main] c.e.managingtransactions.BookingService  : Booking Bob in a seat...
2019-09-19 14:05:26.181  INFO 51911 --- [           main] c.e.managingtransactions.BookingService  : Booking Carol in a seat...
2019-09-19 14:05:26.195  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : Alice, Bob and Carol have been booked
2019-09-19 14:05:26.196  INFO 51911 --- [           main] c.e.managingtransactions.BookingService  : Booking Chris in a seat...
2019-09-19 14:05:26.196  INFO 51911 --- [           main] c.e.managingtransactions.BookingService  : Booking Samuel in a seat...
2019-09-19 14:05:26.271  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : v--- The following exception is expect because 'Samuel' is too big for the DB ---v
2019-09-19 14:05:26.271 ERROR 51911 --- [           main] c.e.managingtransactions.AppRunner       : PreparedStatementCallback; SQL [insert into BOOKINGS(FIRST_NAME) values (?)]; Value too long for column """FIRST_NAME"" VARCHAR(5) NOT NULL": "'Samuel' (6)"; SQL statement:
insert into BOOKINGS(FIRST_NAME) values (?) [22001-199]; nested exception is org.h2.jdbc.JdbcSQLDataException: Value too long for column """FIRST_NAME"" VARCHAR(5) NOT NULL": "'Samuel' (6)"; SQL statement:
insert into BOOKINGS(FIRST_NAME) values (?) [22001-199]
2019-09-19 14:05:26.271  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : So far, Alice is booked.
2019-09-19 14:05:26.271  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : So far, Bob is booked.
2019-09-19 14:05:26.271  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : So far, Carol is booked.
2019-09-19 14:05:26.271  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : You shouldn't see Chris or Samuel. Samuel violated DB constraints, and Chris was rolled back in the same TX
2019-09-19 14:05:26.272  INFO 51911 --- [           main] c.e.managingtransactions.BookingService  : Booking Buddy in a seat...
2019-09-19 14:05:26.272  INFO 51911 --- [           main] c.e.managingtransactions.BookingService  : Booking null in a seat...
2019-09-19 14:05:26.273  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : v--- The following exception is expect because null is not valid for the DB ---v
2019-09-19 14:05:26.273 ERROR 51911 --- [           main] c.e.managingtransactions.AppRunner       : PreparedStatementCallback; SQL [insert into BOOKINGS(FIRST_NAME) values (?)]; NULL not allowed for column "FIRST_NAME"; SQL statement:
insert into BOOKINGS(FIRST_NAME) values (?) [23502-199]; nested exception is org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: NULL not allowed for column "FIRST_NAME"; SQL statement:
insert into BOOKINGS(FIRST_NAME) values (?) [23502-199]
2019-09-19 14:05:26.273  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : So far, Alice is booked.
2019-09-19 14:05:26.273  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : So far, Bob is booked.
2019-09-19 14:05:26.273  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : So far, Carol is booked.
2019-09-19 14:05:26.273  INFO 51911 --- [           main] c.e.managingtransactions.AppRunner       : You shouldn't see Buddy or null. null violated DB constraints, and Buddy was rolled back in the same TX

BOOKINGS テーブルには、first_name 列に 2 つの制約があります。

  • 名前は 5 文字を超えることはできません。

  • 名前を null にすることはできません。

挿入される最初の 3 つの名前は Alice、Bob、Carol です。アプリケーションは、3 人がそのテーブルに追加されたことを表明します。それが機能しなかった場合、アプリケーションは早期に終了していました。

次に、Chris および Samuel の別の予約が行われます。サミュエルの名前は意図的に長すぎて、挿入エラーを強制します。トランザクションの動作では、Chris と Samuel の両方(つまり、このトランザクションのすべての値)をロールバックすることが規定されています。この表にはまだ 3 人しかいないはずです。これはアサーションが示しています。

最後に、Buddy と null が予約されています。出力が示すように、null はロールバックも引き起こし、同じ 3 人が予約されたままになります。

要約

おめでとう! Spring を使用して、非侵入型トランザクションでラップされた単純な JDBC アプリケーションを開発しました。

関連事項

次のガイドも役立つかもしれません:

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください [GitHub] (英語) 。

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の帰属表示、NoDerivatives クリエイティブコモンズライセンス (英語) でリリースされています。

コードを入手する