The JDBC connection object has the ability to create SavePoint objects that are used to rollback a transaction to a specific point in time. One possible use case is providing users the ability to have “Undo” options while working in a database client program. Of course, we can also use SavePoints in Exception handlers or other areas of the program as needed.
SavePoints are used when the connection’s autoCommit property is set to false. We create a SavePoint like so
val bob = connection.setSavePoint("Bob") //Name is optional
Later on, we can pass the SavePoint to the rollback() method on the connection object.
connection.rollback(bob)
Once the connection is rollback to a SavePoint, any work performed on the conncetion after the SavePoint is lost.
Below is an example program that demonstrates using SavePoints.
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>OCJP-DB</groupId> <artifactId>ocjpdb</artifactId> <version>1.0-SNAPSHOT</version> <properties> <kotlin.version>1.2.10</kotlin.version> <main.class>stonesoupprogramming.MainKt</main.class> </properties> <dependencies> <dependency> <groupId>org.apache.derby</groupId> <artifactId>derby</artifactId> <version>10.14.1.0</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jre8</artifactId> <version>${kotlin.version}</version> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-test</artifactId> <version>${kotlin.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <sourceDirectory>src/main/kotlin</sourceDirectory> <plugins> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>${kotlin.version}</version> <executions> <execution> <id>compile</id> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> <execution> <id>test-compile</id> <phase>test-compile</phase> <goals> <goal>test-compile</goal> </goals> </execution> </executions> <configuration> <jvmTarget>1.8</jvmTarget> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.6</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <mainClass>${main.class}</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <executions> <execution> <phase>test</phase> <goals> <goal>java</goal> </goals> </execution> </executions> <configuration> <mainClass>${main.class}</mainClass> </configuration> </plugin> </plugins> </build> </project>
Employees.kt
package stonesoupprogramming import java.sql.Connection import java.sql.DriverManager import java.sql.ResultSet import java.util.* fun main(args: Array<String>) { val properties = Properties() //Populate the properties file with user name and password with(properties) { put("user", "admin") put("password", "pw") } //Open a connection to the database DriverManager .getConnection("jdbc:derby:stonesoup;create=true", properties) .use { connection -> //Set autoCommit to false to manually manage transactions connection.autoCommit = false createOrTruncateTable(connection) //Create an updatable result set val rs = connection .createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE) .executeQuery("SELECT * FROM BURGERS.EMPLOYEES") //Populate the table with data with(rs) { moveToInsertRow() updateInt("ID", 1); updateString("NAME", "Bob") insertRow() val bob = connection.setSavepoint("Bob") moveToInsertRow() updateInt("ID", 2) updateString("NAME", "Linda") insertRow() val linda = connection.setSavepoint("Linda") moveToInsertRow() updateInt("ID", 3) updateString("NAME", "Tina") insertRow() val tina = connection.setSavepoint("Tina") print("Enter Bob, Linda, or Tina => ") val choice = readLine() when (choice) { "Bob" -> connection.rollback(bob) "Linda" -> connection.rollback(linda) "Tina" -> connection.rollback(tina) } } //Commit the transaction connection.commit() //Read only queries are still transactions val rsq = connection .createStatement() .executeQuery("SELECT * FROM BURGERS.EMPLOYEES") with(rsq) { while (next()) { println("${getInt("ID")}\t${getString("NAME")}") } } //So we need to commit this query also even though it doesn't change anything connection.commit() } } private fun createOrTruncateTable(connection: Connection) { val metaData = connection.metaData if (!metaData.getTables(null, "BURGERS", "EMPLOYEES", null).next()) { connection .createStatement() .executeUpdate("CREATE TABLE BURGERS.EMPLOYEES (ID INT PRIMARY KEY, NAME VARCHAR(255))") } else { connection .createStatement() .executeUpdate("TRUNCATE TABLE BURGERS.EMPLOYEES") } connection.commit() }
Explanation
Our program begins by establishing a connection, preparing a table, and creating an updatable ResultSet. We start inserting into the table beginning on line 32. Line 38 is where we create our first SavePoint, after inserting Bob into the table. Once we have the bob SavePoint established, we move on and insert Linda. Linda also gets a SavePoint (line 45), followed by Tina (line 52).
The user is present with a choice on line 55. When they enter Bob, the connection is rolled back to Bob, meaning that neither Linda or Tina are inserted into the database. When the user picks Linda, the connection is rolled back to the linda SavePoint, meaning that Bob and Linda are inserted into the database, but not Tina. If Tina is picked, then all three employees are inserted into the database. Line 64 commits the transaction and the inserts are performed into the database.