This guide walks you through the process of wrapping database operations with non-intrusive transactions.
You will build a simple JDBC application wherein you make database operations transactional without having to write specialized JDBC code.
You can use this pre-initialized project and click Generate to download a ZIP file. This project is configured to fit the examples in this tutorial.
To manually initialize the project:
-
Navigate to https://start.spring.io. This service pulls in all the dependencies you need for an application and does most of the setup for you.
-
Choose either Gradle or Maven and the language you want to use. This guide assumes that you chose Java.
-
Click Dependencies and select Spring Data JDBC and H2 Database.
-
Click Generate.
-
Download the resulting ZIP file, which is an archive of a web application that is configured with your choices.
Note
|
If your IDE has the Spring Initializr integration, you can complete this process from your IDE. |
Note
|
You can also fork the project from Github and open it in your IDE or other editor. |
First, you need to use the BookingService
class to create a JDBC-based service that
books people into the system by name. The following listing (from
src/main/java/com/example/managingtransactions/BookingService.java
) shows how to do so:
link:complete/src/main/java/com/example/managingtransactions/BookingService.java[role=include]
The code has an autowired JdbcTemplate
, a handy template class that does all the
database interactions needed by the remaining code.
You also have a book
method that can book multiple people. It loops through the list of
people and, for each person, inserts that person into the BOOKINGS
table by using the
JdbcTemplate
. This method is tagged with @Transactional
, meaning that any failure
causes the entire operation to roll back to its previous state and to re-throw the
original exception. This means that none of the people are added to BOOKINGS
if one
person fails to be added.
You also have a findAllBookings
method to query the database. Each row fetched from the
database is converted into a String
, and all the rows are assembled into a List
.
The Spring Initializr provides an application class. In this case, you need not modify
this application class. The following listing (from
src/main/java/com/example/managingtransactions/ManagingTransactionsApplication.java
)
shows the application class
link:complete/src/main/java/com/example/managingtransactions/ManagingTransactionsApplication.java[role=include]
Your application actually has zero configuration. Spring Boot detects spring-jdbc
and
h2
on the classpath and automatically creates a DataSource
and a JdbcTemplate
for
you. Because this infrastructure is now available and you have no dedicated configuration,
a DataSourceTransactionManager
is also created for you. This is the component that
intercepts the method annotated with @Transactional
(for example, the book
method on
BookingService
). The BookingService
is detected by classpath scanning.
Another Spring Boot feature demonstrated in this guide is the ability to initialize the schema on startup. The following file (from src/main/resources/schema.sql) defines the database schema:
link:complete/src/main/resources/schema.sql[role=include]
There is also a CommandLineRunner
that injects the BookingService
and
showcases various transactional use cases. The following listing (from
src/main/java/com/example/managingtransactions/AppRunner.java
) shows the command line
runner:
link:complete/src/main/java/com/example/managingtransactions/AppRunner.java[role=include]
You should see the following output:
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
The BOOKINGS
table has two constraints on the first_name
column:
-
Names cannot be longer than five characters.
-
Names cannot be null.
The first three names inserted are Alice
, Bob
, and Carol
. The application asserts
that three people were added to that table. If that had not worked, the application would
have exited early.
Next, another booking is done for Chris
and Samuel
. Samuel’s name is deliberately too
long, forcing an insert error. Transactional behavior stipulates that both Chris
and
Samuel
(that is, all the values in this transaction) should be rolled back. Thus, there
should still be only three people in that table, which the assertion demonstrates.
Finally, Buddy
and null
are booked. As the output shows, null
causes a rollback as
well, leaving the same three people booked.
Congratulations! You have just used Spring to develop a simple JDBC application wrapped with non-intrusive transactions.