Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您完成使用非侵入式事务包装数据库操作的过程。

您将构建的内容

您将构建一个简单的 JDBC 应用程序,在其中您可以对数据库操作进行事务管理,而无需编写专门的 JDBC 代码

所需准备

如何完成本指南

与大多数 Spring 入门指南一样,您可以从头开始并完成每个步骤,也可以跳过您已经熟悉的基本设置步骤。无论哪种方式,您最终都能获得可运行的代码。

从头开始,请继续阅读 从 Spring Initializr 开始

跳过基础部分,请执行以下操作:

完成后,您可以将您的结果与 gs-managing-transactions/complete 中的代码进行对比。

使用 Spring Initializr 开始

您可以使用这个预初始化项目,并点击生成以下载一个ZIP文件。该项目已配置为适合本教程中的示例。

要手动初始化项目:

  1. 访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置工作。

  2. 选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies,然后选择 Spring Data JDBCH2 Database

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,这是一个根据您的选择配置的 Web 应用程序存档。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成这个过程。

您也可以从 Github 上 fork 这个项目,并在您的 IDE 或其他编辑器中打开它。

创建预订服务

首先,您需要使用 BookingService 类来创建一个基于 JDBC 的服务,用于按姓名将人员预订到系统中。以下代码清单(来自 src/main/java/com/example/managingtransactions/BookingService.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 " + person + " in a seat...");
      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"));
  }

}

代码中有一个自动注入的 JdbcTemplate,这是一个方便的模板类,负责处理剩余代码所需的所有数据库交互。

您还有一个 book 方法,可以预订多个人。它会遍历人员列表,并使用 JdbcTemplate 将每个人插入到 BOOKINGS 表中。该方法被标记为 @Transactional,这意味着任何失败都会导致整个操作回滚到之前的状态,并重新抛出原始异常。因此,如果一个人无法被添加,则不会将任何人添加到 BOOKINGS 表中。

此外,您还有一个 findAllBookings 方法用于查询数据库。从数据库中获取的每一行都会被转换为一个 String,所有行会被组装成一个 List

构建一个应用程序

Spring Initializr 提供了一个应用程序类。在这种情况下,您无需修改此应用程序类。以下清单(来自 src/main/java/com/example/managingtransactions/ManagingTransactionsApplication.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);
  }

}

@SpringBootApplication 是一个便捷的注解,它添加了以下所有功能:

  • @Configuration: 将该类标记为应用程序上下文的 Bean 定义源。

  • @EnableAutoConfiguration: 指示 Spring Boot 根据类路径设置、其他 Bean 和各种属性设置开始添加 Bean。例如,如果类路径上有 spring-webmvc,该注解会将应用程序标记为 Web 应用程序,并激活关键行为,例如设置 DispatcherServlet

  • @ComponentScan: 指示 Spring 在 com/example 包中查找其他组件、配置和服务,使其能够找到控制器。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法来启动应用程序。您是否注意到没有一行 XML 代码?也没有 web.xml 文件。这个 Web 应用程序是 100% 纯 Java 的,您不需要处理任何配置或基础设施的设置。

您的应用程序实际上没有任何配置。Spring Boot 检测到类路径上的 spring-jdbch2,并自动为您创建了 DataSourceJdbcTemplate。由于这些基础设施现在可用,并且您没有进行专门配置,因此还为您创建了一个 DataSourceTransactionManager。这个组件会拦截带有 @Transactional 注解的方法(例如,BookingService 上的 book 方法)。BookingService 是通过类路径扫描检测到的。

本指南中展示的另一个 Spring Boot 功能是启动时初始化 schema 的能力。以下文件(来自 src/main/resources/schema.sql)定义了数据库 schema:

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

还有一个 CommandLineRunner,它注入了 BookingService 并展示了各种事务用例。以下清单(来自 src/main/java/com/example/managingtransactions/AppRunner.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());
    }

    for (String person : bookingService.findAllBookings()) {
      logger.info("So far, " + person + " is booked.");
    }
    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());
    }

    for (String person : bookingService.findAllBookings()) {
      logger.info("So far, " + person + " is booked.");
    }
    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");
  }

}

您可以使用 Gradle 或 Maven 从命令行运行应用程序。您还可以构建一个包含所有必要依赖项、类和资源的可执行 JAR 文件并运行它。构建可执行 JAR 文件可以方便地在整个开发生命周期中、跨不同环境等情况下作为应用程序进行发布、版本控制和部署。

如果您使用 Gradle,可以通过 ./gradlew bootRun 运行应用程序。或者,您可以使用 ./gradlew build 构建 JAR 文件,然后按如下方式运行 JAR 文件:

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

如果您使用 Maven,可以通过 ./mvnw spring-boot:run 来运行应用程序。或者,您可以使用 ./mvnw clean package 构建 JAR 文件,然后按照以下方式运行该 JAR 文件:

java -jar target/gs-managing-transactions-0.1.0.jar

这里描述的步骤将创建一个可运行的 JAR 文件。您也可以构建一个经典的 WAR 文件

您应该会看到以下输出:

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 列上有两个约束:

  • 名称长度不能超过五个字符。

  • 名称不能为空。

最初插入的三个名字是 AliceBobCarol。应用程序断言有三个人被添加到该表中。如果操作未成功,应用程序将提前退出。

接下来,为 ChrisSamuel 进行了另一次预订。Samuel 的名字故意设计得过长,导致插入错误。事务行为规定,ChrisSamuel(即此事务中的所有值)都应回滚。因此,表中应该仍然只有三个人,断言证明了这一点。

最后,预订了 Buddynull。如输出所示,null 同样会导致回滚,最终表中仍然只有三个人被预订。

总结

恭喜!您刚刚使用 Spring 开发了一个带有非侵入式事务的简单 JDBC 应用程序。

另请参阅

以下指南可能也会有帮助:

本页目录