Учебные материалы

Управление транзакциями

В этом уроке освещается процесс упаковки операций с базой данных в транцакции.

Что вы создадите

Вы создадите простое JDBC приложение, в котором выполняются транзакционные операции в БД без написания конкретного JDBC кода.

Что вам потребуется

  • Примерно 15 минут свободного времени
  • Любимый текстовый редактор или IDE
  • JDK 6 и выше
  • Gradle 1.11+ или Maven 3.0+
  • Вы также можете импортировать код этого урока, а также просматривать web-страницы прямо из Spring Tool Suite (STS), собственно как и работать дальше из него.

Как проходить этот урок

Как и большинство уроков по Spring, вы можете начать с нуля и выполнять каждый шаг, либо пропустить базовые шаги, которые вам уже знакомы. В любом случае, вы в конечном итоге получите рабочий код.

Чтобы начать с нуля, перейдите в Настройка проекта.

Чтобы пропустить базовые шаги, выполните следующее:

Когда вы закончите, можете сравнить получившийся результат с образцом в gs-managing-transactions/complete.

Настройка проекта

Для начала вам необходимо настроить базовый скрипт сборки. Вы можете использовать любую систему сборки, которая вам нравится для сборки проетов Spring, но в этом уроке рассмотрим код для работы с Gradle и Maven. Если вы не знакомы ни с одним из них, ознакомьтесь с соответсвующими уроками Сборка Java-проекта с использованием Gradle или Сборка Java-проекта с использованием Maven.

Создание структуры каталогов

В выбранном вами каталоге проекта создайте следующую структуру каталогов; к примеру, командой mkdir -p src/main/java/hello для *nix систем:

└── src
    └── main
        └── java
            └── hello

Создание файла сборки Gradle

Ниже представлен начальный файл сборки Gradle. Файл pom.xml находится здесь. Если вы используете Spring Tool Suite (STS), то можете импортировать урок прямо из него.

Если вы посмотрите на pom.xml, вы найдете, что указана версия для maven-compiler-plugin. В общем, это не рекомендуется делать. В данном случае он предназначен для решения проблем с нашей CI системы, которая по умолчанию имеет старую(до Java 5) версию этого плагина.

build.gradle

buildscript {
    repositories {
        maven { url "http://repo.spring.io/libs-release" }
        mavenLocal()
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.8.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'gs-managing-transactions'
    version =  '0.1.0'
}

repositories {
    mavenLocal()
    mavenCentral()
    maven { url "http://repo.spring.io/libs-release" }
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter")
    compile("org.springframework:spring-tx")
    compile("org.springframework:spring-jdbc")
    compile("com.h2database:h2")
    compile("junit:junit")
}

task wrapper(type: Wrapper) {
    gradleVersion = '1.11'
}

Spring Boot gradle plugin предоставляет множество удобных возможностей:

  • Он собирает все jar'ы в classpath и собирает единое, исполняемое "über-jar", что делает более удобным выполнение и доставку вашего сервиса
  • Он ищет public static void main() метод, как признак исполняемого класса
  • Он предоставляет встроенное разрешение зависимостей, с определенными номерами версий для соответсвующих Spring Boot зависимостей. Вы можете переопределить на любые версии, какие захотите, но он будет по умолчанию для Boot выбранным набором версий

Создание сервиса бронирования

Для начала, используйте BookingService для создания JDBC-сервиса, бронирует людей в системе по имени.

src/main/java/hello/BookingService.java

package hello;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.annotation.Transactional;

public class BookingService {

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

    @Autowired
    JdbcTemplate jdbcTemplate;

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

    public List<String7gt; findAllBookings() {
        return jdbcTemplate.query("select FIRST_NAME from BOOKINGS", new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getString("FIRST_NAME");
            }
        });
    }

}

Класс имеет введенный JdbcTemplate, удобный шаблонный класс, который осуществляет все требуемые взаимодействия с БД, необходимые в коде, описанным ниже.

Также есть метод book, предназначенный для бронирования нескольких людей. Он перебирает весь список людей и каждого человека вставляет в таблицу BOOKINGS, используя JdbcTemplate. Этот метод помечен как @Transactional, что означает откат всех записей к предыдущему значению, если любая из операций в этом методе завершится неудачей, а также повторно бросит оригинальное исключение. Это значит, что если добавление одного из людей завершится ошибкой, то ни один из людей в итоге не добавится в таблицу BOOKINGS.

Также имеется метод findAllBookings для запроса к БД. Каждая запись, полученная из БД, конвертируется в String, а затем помещается в List.

Сборка приложения

src/main/java/hello/Application.java

package hello;

import javax.sql.DataSource;

import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
@EnableAutoConfiguration
public class Application {

	private static final Logger log = LoggerFactory.getLogger(Application.class);

	@Bean
	BookingService bookingService() {
		return new BookingService();
	}

	@Bean
	DataSource dataSource() {
		return new SimpleDriverDataSource() {
			{
				setDriverClass(org.h2.Driver.class);
				setUsername("sa");
				setUrl("jdbc:h2:mem");
				setPassword("");
			}
		};
	}

	@Bean
	JdbcTemplate jdbcTemplate(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		log.info("Creating tables");
		jdbcTemplate.execute("drop table BOOKINGS if exists");
		jdbcTemplate.execute("create table BOOKINGS("
				+ "ID serial, FIRST_NAME varchar(5) NOT NULL)");
		return jdbcTemplate;
	}

	public static void main(String[] args) {
		ApplicationContext ctx = SpringApplication.run(Application.class, args);

		BookingService bookingService = ctx.getBean(BookingService.class);
		bookingService.book("Alice", "Bob", "Carol");
		Assert.assertEquals("First booking should work with no problem", 3,
				bookingService.findAllBookings().size());

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

		for (String person : bookingService.findAllBookings()) {
			log.info("So far, " + person + " is booked.");
		}
		log.info("You shouldn't see Chris or Samuel. Samuel violated DB constraints, and Chris was rolled back in the same TX");
		Assert.assertEquals("'Samuel' should have triggered a rollback", 3,
				bookingService.findAllBookings().size());

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

		for (String person : bookingService.findAllBookings()) {
			log.info("So far, " + person + " is booked.");
		}
		log.info("You shouldn't see Buddy or null. null violated DB constraints, and Buddy was rolled back in the same TX");
		Assert.assertEquals("'null' should have triggered a rollback", 3, bookingService
				.findAllBookings().size());

	}

}

Вы настраиваете ваши бины в конфигурационном классе Application. Метод bookingService инициализирует BookingService.

Как показано ранее, JdbcTemplate введен в BookingService, поэтому вам не нужно определять его в коде класса Application:

SimpleDriverDataSource является удобным классом, но он не предназначен для рабочего использования. Для этого обычно используют какой-то пул JDBC соединений для обработки множества параллельных запросов.

В методе jdbcTemplate вы создаете экземпляр JdbcTemplate, также содержащий DDL для описания таблицы BOOKING.

В рабочих системах таблицы БД обычно описываются вне приложения.

Метод main() передает управление вспомогательному классу SpringApplication, предоставляя Application.class как аргумент его run() методу. Это говорит Spring о том, чтобы прочитать аннотацию метаданных из Application и управлять им как компонентом в Spring Application Context.

обратите внимание на две особенно полезные части этой конфигурации приложения:

  • @EnableTransactionManagement активирует возможности Spring бесшовной транзакции через @Transactional
  • EnableAutoConfiguration переключает на соответствующее поведение по умолчанию в зависимости от содержания вашего classpath. К примеру, она определяет, что у вас spring-tx, а также DataSource и автоматически создает другие бины, необходимые для транзакций. Автоконфигурация является полезным и гибким механизмом. Более подробную информацию смотрите на странице API документации

Сборка исполняемого JAR

Вы можете собрать единый исполняемый JAR-файл, который содержит все необходимые зависимости, классы и ресурсы. Это делает его легким в загрузке, версионировании и развертывании сервиса как приложения на протяжении всего периода разработки, на различных средах и так далее.

./gradlew build

Затем вы можете запустить JAR-файл:

java -jar build/libs/gs-managing-transactions-0.1.0.jar

Если вы используете Maven, вы можете запустить приложение, используя mvn spring-boot:run, либо вы можете собрать приложение с mvn clean package и запустить JAR примерно так:

java -jar target/gs-managing-transactions-0.1.0.jar
Процедура, описанная выше, создает исполняемый JAR. Вы также можете вместо него собрать классический WAR-файл.

Если вы используете Gradle, вы можете запустить ваш сервис из командной строки:

./gradlew clean build && java -jar build/libs/gs-managing-transactions-0.1.0.jar
Если вы используете Maven, то можете запустить ваш сервис таким образом: mvn clean package && java -jar target/gs-managing-transactions-0.1.0.jar.

Как вариант, вы можете запустить ваш сервис напрямую из Gradle примерно так:

./gradlew bootRun
С mvn - mvn spring-boot:run.

Вы должны увидеть следующее:

2014-08-28 10:49:20.935  INFO 24084 --- [           main] hello.Application                        : Creating tables
2014-08-28 10:49:21.347  INFO 24084 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-08-28 10:49:21.389  INFO 24084 --- [           main] hello.Application                        : Started Application in 1.488 seconds (JVM running for 1.772)
2014-08-28 10:49:21.443  INFO 24084 --- [           main] hello.BookingService                     : Booking Alice in a seat...
2014-08-28 10:49:21.447  INFO 24084 --- [           main] hello.BookingService                     : Booking Bob in a seat...
2014-08-28 10:49:21.447  INFO 24084 --- [           main] hello.BookingService                     : Booking Carol in a seat...
2014-08-28 10:49:21.536  INFO 24084 --- [           main] hello.BookingService                     : Booking Chris in a seat...
2014-08-28 10:49:21.537  INFO 24084 --- [           main] hello.BookingService                     : Booking Samuel in a seat...
2014-08-28 10:49:21.545  INFO 24084 --- [           main] o.s.b.f.xml.XmlBeanDefinitionReader      : Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
2014-08-28 10:49:21.612  INFO 24084 --- [           main] o.s.jdbc.support.SQLErrorCodesFactory    : SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
2014-08-28 10:49:21.623  INFO 24084 --- [           main] hello.Application                        : v--- The following exception is expect because 'Samuel' is too big for the DB ---v
2014-08-28 10:49:21.623 ERROR 24084 --- [           main] hello.Application                        : 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-176]; nested exception is org.h2.jdbc.JdbcSQLException: Value too long for column "FIRST_NAME VARCHAR(5) NOT NULL": "'Samuel' (6)"; SQL statement:
insert into BOOKINGS(FIRST_NAME) values (?) [22001-176]
2014-08-28 10:49:21.661  INFO 24084 --- [           main] hello.Application                        : So far, Alice is booked.
2014-08-28 10:49:21.661  INFO 24084 --- [           main] hello.Application                        : So far, Bob is booked.
2014-08-28 10:49:21.662  INFO 24084 --- [           main] hello.Application                        : So far, Carol is booked.
2014-08-28 10:49:21.662  INFO 24084 --- [           main] hello.Application                        : You shouldn't see Chris or Samuel. Samuel violated DB constraints, and Chris was rolled back in the same TX
2014-08-28 10:49:21.730  INFO 24084 --- [           main] hello.BookingService                     : Booking Buddy in a seat...
2014-08-28 10:49:21.731  INFO 24084 --- [           main] hello.BookingService                     : Booking null in a seat...
2014-08-28 10:49:21.735  INFO 24084 --- [           main] hello.Application                        : v--- The following exception is expect because null is not valid for the DB ---v
2014-08-28 10:49:21.736 ERROR 24084 --- [           main] hello.Application                        : PreparedStatementCallback; SQL [insert into BOOKINGS(FIRST_NAME) values (?)]; NULL not allowed for column "FIRST_NAME"; SQL statement:
insert into BOOKINGS(FIRST_NAME) values (?) [23502-176]; nested exception is org.h2.jdbc.JdbcSQLException: NULL not allowed for column "FIRST_NAME"; SQL statement:
insert into BOOKINGS(FIRST_NAME) values (?) [23502-176]
2014-08-28 10:49:21.771  INFO 24084 --- [           main] hello.Application                        : So far, Alice is booked.
2014-08-28 10:49:21.772  INFO 24084 --- [           main] hello.Application                        : So far, Bob is booked.
2014-08-28 10:49:21.772  INFO 24084 --- [           main] hello.Application                        : So far, Carol is booked.
2014-08-28 10:49:21.772  INFO 24084 --- [           main] hello.Application                        : You shouldn't see Buddy or null. null violated DB constraints, and Buddy was rolled back in the same TX

Таблица BOOKING имеет два ограничения по колоке first_name:

  • Имена не должны быть больше пяти символов
  • Имена не должны быть со значением null

Первые три имени вставлены - Alice, Bob и Carol. Приложение проверяет, что три человека были добавлены в таблицу. Если это не так, приложение завершится раньше.

Далее, бронируется для Chris и Samuel. Имя Samuel'я специально сделано длинным, чтобы вызвать ошибку. Транзакционное Поведение предусматривает что оба в этой транзакции будут отброшены. Т.о. в таблице должно быть все ещё трое людей, что демонстрирует проверка.

В заключении бронируются Buddy и null. Как показано в выходных данных, null отбразывается, при этом также остается забронировано трое человек.

Итог

Поздравляем! Вы только что использовали Spring для разработки простого JDBC приложения, которое обернуто в бесшовные транзакции.

С оригинальным текстом урока вы можете ознакомиться на spring.io.

comments powered by Disqus