本指南介绍如何以非侵入式事务包裹数据库操作。
你将构建什么
你将构建一个简单的 JDBC 应用,不必编写专门的 JDBC 事务代码,就能让数据库操作具备事务性。
所需条件
- 约 15 分钟。
- 你喜欢的文本编辑器或 IDE。
- Java 17 或更高版本。
- Gradle 7.5+ 或 Maven 3.5+。
- 也可以将代码直接导入 Spring Tool Suite(STS)、IntelliJ IDEA 或 VSCode。
如何完成本指南
与大多数 Spring 入门指南一样,你可以从零开始逐步完成,也可以跳过已经熟悉的基础设置。不论哪种方式,最终都会得到可以工作的代码。
如果从零开始,请继续阅读“从 Spring Initializr 开始”。
如果要跳过基础设置,请执行以下步骤:
- 下载并解压本指南的源码仓库,或通过 Git 克隆:
git clone https://github.com/spring-guides/gs-managing-transactions.git。 - 使用
cd进入gs-managing-transactions/initial。 - 直接跳到“创建预约服务”。
完成后,可以与 gs-managing-transactions/complete 中的代码比较。
从 Spring Initializr 开始
你可以使用预先初始化的项目,点击 Generate 下载 ZIP 文件。这个项目已经配置为适合本教程示例的形式。
如果手动初始化项目:
- 访问 https://start.spring.io。该服务会引入应用所需的全部依赖,并完成大部分设置。
- 选择 Gradle 或 Maven,以及希望使用的语言。
- 在 Artifact 字段中输入
managing-transactions。 - 点击 Dependencies,选择 Spring Data JDBC 与 H2 Database。
- 点击 Generate。
- 下载生成的 ZIP 文件,其中包含根据所选选项配置的应用项目。
如果 IDE 集成了 Spring Initializr,可以直接在 IDE 中完成这一过程。
也可以在 GitHub 上 fork 项目,再用 IDE 或其他编辑器打开。
创建预约服务
首先使用 BookingService 类创建一个基于 JDBC 的服务,按姓名把人预约到系统中。以下分别保留 Java 与 Kotlin 实现。
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;在本例的数据库写入失败场景中,整个操作会回滚到之前状态,并重新抛出原异常。也就是说,只要其中一人添加失败,这次调用中的任何人都不会添加到 BOOKINGS。
findAllBookings 方法负责查询数据库,把每一行转换为 String,再将全部行组合为 List。
构建应用
Spring Initializr 提供应用类。本例无需修改它。以下分别保留 Java 与 Kotlin 应用类。
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:告诉 Spring Boot 根据类路径、其他 bean 和属性设置添加 bean。例如,类路径包含 spring-webmvc 时,它会将应用标记为 Web 应用,并启用设置 DispatcherServlet 等关键行为。@ComponentScan:让 Spring 在 com/example 包中寻找其他组件、配置和服务,从而发现控制器。
main() 使用 Spring Boot 的 SpringApplication.run() 启动应用。这里没有一行 XML,也没有 web.xml 文件。这个应用完全使用 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);
还有一个 CommandLineRunner,它注入 BookingService,展示不同事务使用场景。以下分别保留 Java 与 Kotlin 实现:
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,再按下方命令运行:
java -jar build/libs/gs-managing-transactions-0.0.1-SNAPSHOT.jar
使用 Maven 时,运行 ./mvnw spring-boot:run 即可启动。也可以通过 ./mvnw clean package 构建 JAR,再按下方命令运行:
java -jar target/gs-managing-transactions-0.0.1-SNAPSHOT.jar
JAR 文件名:上面命令中的文件名对应本指南源码仓库。若使用 Spring Initializr 生成项目,应按项目的 Artifact 和版本号,使用构建目录内实际生成的 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 列有两个约束:
- 姓名不能超过五个字符。
- 姓名不能为 null。
首先插入 Alice、Bob 和 Carol。应用断言表中新增了三人。如果这一步失败,应用就会提前退出。
接下来预约 Chris 和 Samuel。Samuel 的姓名被故意设置得过长,触发插入错误。事务行为要求 Chris 和 Samuel,也就是本次事务中的所有值,一起回滚。因此表中仍然只有三人,断言验证了这一点。
最后预约 Buddy 和 null。示例输出表明,null 同样触发回滚,预约列表仍然是原来的三人。
小结
你已经使用 Spring 构建了一个由非侵入式事务包裹的简单 JDBC 应用。
另请参阅
以下指南也可能有所帮助:
如果想编写新指南或为已有指南贡献内容,请查看贡献指南。
所有指南的代码采用 ASLv2(Apache License 2.0)许可证;文字采用知识共享署名—禁止演绎 3.0(CC BY-ND 3.0)许可证。
原文:Spring — Managing Transactions,Spring 团队。中文译文。代码与原文字的许可分别见上方;示例日志来自 Spring 原文。












暂无评论内容