本指南将引导您完成使用非侵入式事务包装数据库操作的过程。
您将构建的内容
您将构建一个简单的 JDBC 应用程序,在其中您可以对数据库操作进行事务管理,而无需编写专门的 JDBC 代码。
所需准备
-
大约15分钟
-
常用的文本编辑器或 IDE
-
Java 17 或更高版本
-
您还可以直接将代码导入到您的 IDE 中:
如何完成本指南
与大多数 Spring 入门指南一样,您可以从头开始并完成每个步骤,也可以跳过您已经熟悉的基本设置步骤。无论哪种方式,您最终都能获得可运行的代码。
要从头开始,请继续阅读 从 Spring Initializr 开始。
要跳过基础部分,请执行以下操作:
- 下载并解压本指南的源代码仓库,或使用 Git 克隆它:
git clone https://github.com/spring-guides/gs-managing-transactions.git
- 进入
gs-managing-transactions/initial
目录 - 跳转到 创建预订服务。
完成后,您可以将您的结果与 gs-managing-transactions/complete
中的代码进行对比。
使用 Spring Initializr 开始
您可以使用这个预初始化项目,并点击生成以下载一个ZIP文件。该项目已配置为适合本教程中的示例。
要手动初始化项目:
-
访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置工作。
-
选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。
-
点击 Dependencies,然后选择 Spring Data JDBC 和 H2 Database。
-
点击 Generate。
-
下载生成的 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-jdbc
和 h2
,并自动为您创建了 DataSource
和 JdbcTemplate
。由于这些基础设施现在可用,并且您没有进行专门配置,因此还为您创建了一个 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
列上有两个约束:
-
名称长度不能超过五个字符。
-
名称不能为空。
最初插入的三个名字是 Alice
、Bob
和 Carol
。应用程序断言有三个人被添加到该表中。如果操作未成功,应用程序将提前退出。
接下来,为 Chris
和 Samuel
进行了另一次预订。Samuel
的名字故意设计得过长,导致插入错误。事务行为规定,Chris
和 Samuel
(即此事务中的所有值)都应回滚。因此,表中应该仍然只有三个人,断言证明了这一点。
最后,预订了 Buddy
和 null
。如输出所示,null
同样会导致回滚,最终表中仍然只有三个人被预订。
总结
恭喜!您刚刚使用 Spring 开发了一个带有非侵入式事务的简单 JDBC 应用程序。
另请参阅
以下指南可能也会有帮助: