这篇文章里,我们来讨论一些和JDBC相关的话题。
概述
尽管在实际开发过程中,我们一般使用ORM框架来代替传统的JDBC,例如Hibernate或者iBatis,但JDBC是Java用来实现数据访问的基础,掌握它对于我们理解Java的数据操作流程很有帮助。
JDBC的全称是Java Database Connectivity。
JDBC对数据库进行操作的流程:
- 连接数据库
- 发送数据请求,即传统的CRUD指令
- 返回操作结果集
JDBC中常用的对象包括:
- ConnectionManager
- Connection
- Statement
- CallableStatement
- PreparedStatement
- ResultSet
- SavePoint
一个简单示例
我们来看下面一个简单的示例,它使用JDK自带的Derby数据库,创建一张表,插入一些记录,然后将记录返回:
1 private static void test1() throws SQLException 2 { 3 String driver = "org.apache.derby.jdbc.EmbeddedDriver"; 4 String dbURL = "jdbc:derby:EmbeddedDB;create=true"; 5 6 Connection con = null; 7 Statement st = null; 8 try 9 { 10 Class.forName(driver); 11 con = DriverManager.getConnection(dbURL); 12 st = con.createStatement(); 13 st.execute("create table foo(ID INT NOT NULL, NAME VARCHAR(30))"); 14 st.executeUpdate("insert into foo(ID,NAME) values(1, 'Zhang San')"); 15 16 ResultSet rs = st.executeQuery("select ID,NAME from foo"); 17 18 while(rs.next()) 19 { 20 int id = rs.getInt("ID"); 21 String name = rs.getString("NAME"); 22 System.out.println("ID=" + id + "; NAME=" + name); 23 } 24 } 25 catch(Exception ex) 26 { 27 ex.printStackTrace(); 28 } 29 finally 30 { 31 if (st != null) st.close(); 32 if (con != null) con.close(); 33 } 34 }
如何建立数据库连接
上面的示例代码中,建立数据库连接的部分如下:
String driver = "org.apache.derby.jdbc.EmbeddedDriver"; String dbURL = "jdbc:derby:EmbeddedDB;create=true"; Class.forName(driver); con = DriverManager.getConnection(dbURL);
建立数据库连接的过程,可以分为两步:
1)加载数据库驱动,即上文中的driver以及Class.forName(dirver)
2)定位数据库连接字符串, 即dbURL以及DriverManager.getConnection(dbURL)
不同的数据库,对应的dirver和dbURL不同,但加载驱动和建立连接的方式是相同的,即只需要修改上面driver和dbURL的值就可以了。
自动加载数据库驱动
如果我们每次建立连接时,都要使用Class.forName(...)来手动加载数据库驱动,这样会很麻烦,我们可以通过配置文件的方式,来保存数据库驱动的信息。
我们可以在classpath中,即编译出来的.class的存放路径,添加如下文件:
META-INF\services\java.sql.Driver
对应的内容就是JDBC驱动的全路径,也就是上面driver变量的值:
org.apache.derby.jdbc.EmbeddedDriver
接下来,我们在程序中,就不需要再显示的用Class.forName(...)来加载驱动了,它会被自动加载进来,当我们的数据库发生变化时,只需要修改这个文件就可以了,例如当我们的数据库由Derby变为MySQL时,只需要将上述的配置修改为:
com.mysql.jdbc.Driver
但是,需要注意一点,这里只是配置了JDBC驱动的全路径,并没有包含jar文件的信息,因此,我们还是需要将包含该驱动的jar文件手动的放置到程序的classpath中。
JDBC中的基本操作
对于数据库操作来说,CRUD操作应该是最常见的操作了, 即我们常说的增、删、查、改。
JDBC是使用Statement和ResultSet来完成这些操作的。
如何实现CRUD
下面是一个实现CRUD的示例:
1 private static void insertTest() throws SQLException 2 { 3 String dbURL = "jdbc:mysql://localhost/test"; 4 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 5 Statement st = con.createStatement(); 6 st.execute("insert into user(ID,NAME) values(1, 'Zhang San')"); 7 st.execute("insert into user(ID,NAME) values(2, 'Li Si')"); 8 st.execute("insert into user(ID,NAME) values(3, 'Wang Wu')"); 9 System.out.println("=====insert test====="); 10 showUser(st); 11 st.close(); 12 con.close(); 13 } 14 15 private static void deleteTest() throws SQLException 16 { 17 String dbURL = "jdbc:mysql://localhost/test"; 18 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 19 Statement st = con.createStatement(); 20 st.execute("delete from user where ID=3"); 21 System.out.println("=====delete test====="); 22 showUser(st); 23 st.close(); 24 con.close(); 25 } 26 27 private static void updateTest() throws SQLException 28 { 29 String dbURL = "jdbc:mysql://localhost/test"; 30 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 31 Statement st = con.createStatement(); 32 st.executeUpdate("update user set NAME='TEST' where ID=2"); 33 System.out.println("=====update test====="); 34 showUser(st); 35 st.close(); 36 con.close(); 37 } 38 39 private static void showUser(Statement st) throws SQLException 40 { 41 ResultSet rs = st.executeQuery("select ID, NAME from user"); 42 while(rs.next()) 43 { 44 int id = rs.getInt("ID"); 45 String name = rs.getString("NAME"); 46 System.out.println("ID:" + id + "; NAME=" + name); 47 } 48 rs.close(); 49 }
我们顺序调用上面的测试方法:
1 insertTest(); 2 deleteTest(); 3 updateTest();
执行结果如下:
=====insert test===== ID:1; NAME=Zhang San ID:2; NAME=Li Si ID:3; NAME=Wang Wu =====delete test===== ID:1; NAME=Zhang San ID:2; NAME=Li Si =====update test===== ID:1; NAME=Zhang San ID:2; NAME=TEST
上面代码中的showUser方法会把user表中的所有记录打印出来。
如何调用存储过程
存储过程是做数据库开发时经常使用的技术,它可以通过节省编译时间的方式来提升系统性能,我们这里的示例使用MySQL数据库。
如何调用不带参数的存储过程
假设我们现在有一个简单的存储过程,它只是返回user表中的所有记录,存储过程如下:
1 CREATE DEFINER=`root`@`localhost` PROCEDURE `GetUser`() 2 BEGIN 3 select ID,NAME from user; 4 END
我们可以使用CallableStatement来调用存储过程:
1 private static void execStoredProcedureTest() throws SQLException 2 { 3 String dbURL = "jdbc:mysql://localhost/test"; 4 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 5 CallableStatement cst = con.prepareCall("call GetUser()"); 6 ResultSet rs = cst.executeQuery(); 7 while(rs.next()) 8 { 9 int id = rs.getInt("ID"); 10 String name = rs.getString("NAME"); 11 System.out.println("ID:" + id + "; NAME=" + name); 12 } 13 rs.close(); 14 cst.close(); 15 con.close(); 16 }
它的执行结果如下:
ID:1; NAME=Zhang San
ID:2; NAME=TEST
如何调用带参数的存储过程
MySQL的存储过程中的参数分为三种:in/out/inout,我们可以把in看做入力参数,out看做出力参数,JDBC对这两种类型的参数设置方式不同:
1)in, JDBC使用类似于cst.set(1, 10)的方式来设置
2)out,JDBC使用类似于cst.registerOutParameter(2, Types.VARCHAR);的方式来设置
我们来看一个in参数的示例,假设我们希望返回ID为特定值的user信息,存储过程如下:
1 CREATE DEFINER=`root`@`localhost` PROCEDURE `GetUserByID`(in id int) 2 BEGIN 3 set @sqlstr=concat('select * from user where ID=', id); 4 prepare psmt from @sqlstr; 5 execute psmt; 6 END
Java的调用代码如下:
1 private static void execStoredProcedureTest2(int id) throws SQLException 2 { 3 String dbURL = "jdbc:mysql://localhost/test"; 4 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 5 CallableStatement cst = con.prepareCall("call GetUserByID(?)"); 6 cst.setInt(1, id); 7 ResultSet rs = cst.executeQuery(); 8 while(rs.next()) 9 { 10 String name = rs.getString("NAME"); 11 System.out.println("ID:" + id + "; NAME=" + name); 12 } 13 rs.close(); 14 cst.close(); 15 con.close(); 16 }
我们执行下面的语句:
execStoredProcedureTest2(1);
结果如下:
ID:1; NAME=Zhang San
对于out类型的参数,调用方式类似,不再赘述。
获取数据库以及结果集的metadata信息
在JDBC中,我们不仅能够对数据进行操作,我们还能获取数据库以及结果集的元数据信息,例如数据库的名称、驱动信息、表信息;结果集的列信息等。
获取数据库的metadata信息
我们可以通过connection.getMetaData方法来获取数据库的元数据信息,它的类型是DatabaseMetaData。
1 private static void test1() throws SQLException 2 { 3 String dbURL = "jdbc:mysql://localhost/mysql"; 4 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 5 6 DatabaseMetaData dbmd = con.getMetaData(); 7 8 System.out.println("数据库:" + dbmd.getDatabaseProductName() + " " + dbmd.getDatabaseProductVersion()); 9 System.out.println("驱动程序:" + dbmd.getDriverName() + " " + dbmd.getDriverVersion()); 10 11 ResultSet rs = dbmd.getTables(null, null, null, null); 12 System.out.println(String.format("|%-26s|%-9s|%-9s|%-9s|", "表名称","表类别","表类型","表模式")); 13 while(rs.next()) 14 { 15 System.out.println(String.format("|%-25s|%-10s|%-10s|%-10s|", 16 rs.getString("TABLE_NAME"),rs.getString("TABLE_CAT"), 17 rs.getString("TABLE_TYPE"), rs.getString("TABLE_SCHEM"))); 18 } 19 }
这里我们使用的数据库是MySQL中自带的默认数据库:mysql,它会记录整个数据库服务器中的一些信息。上述代码执行结果如下:
数据库:MySQL 5.5.28 驱动程序:MySQL-AB JDBC Driver mysql-connector-java-5.0.4 ( $Date: 2006-10-19 17:47:48 +0200 (Thu, 19 Oct 2006) $, $Revision: 5908 $ ) |表名称 |表类别 |表类型 |表模式 | |columns_priv |mysql |TABLE |null | |db |mysql |TABLE |null | |event |mysql |TABLE |null | |func |mysql |TABLE |null | 。。。
由于mysql中表比较多,上述结果只截取了一部分。
获取结果集的元数据信息
我们可以通过使用resultset.getMetaData方法来获取结果集的元数据信息,它的类型是ResultSetMetaData。
1 private static void test2() throws SQLException 2 { 3 String dbURL = "jdbc:mysql://localhost/test"; 4 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 5 Statement st = con.createStatement(); 6 ResultSet rs = st.executeQuery("select ID, NAME from user"); 7 ResultSetMetaData rsmd = rs.getMetaData(); 8 for (int i = 1; i <= rsmd.getColumnCount(); i++) 9 { 10 System.out.println("Column Name:" + rsmd.getColumnName(i) + "; Column Type:" + rsmd.getColumnTypeName(i)); 11 } 12 }
它的执行结果如下:
Column Name:ID; Column Type:INTEGER UNSIGNED
Column Name:NAME; Column Type:VARCHAR
可以看到,它返回类结果集中每一列的名称和类型。
基于ResultSet的操作
当我们需要对数据库进行修改时,除了上述通过Statement完成操作外,我们也可以借助ResultSet来完成。
需要注意的是,在这种情况下,我们定义Statement时,需要添加参数。
Statement构造函数可以包含3个参数:
-
resultSetType
,它的取值包括:ResultSet.TYPE_FORWARD_ONLY
、ResultSet.TYPE_SCROLL_INSENSITIVE
或ResultSet.TYPE_SCROLL_SENSITIVE,默认情况下,该参数的值是
ResultSet.TYPE_FORWARD_ONLY
。 -
resultSetConcurrency
,它的取值包括:ResultSet.CONCUR_READ_ONLY
或ResultSet.CONCUR_UPDATABLE
,默认情况下,该参数的值是ResultSet.CONCUR_READ_ONLY
。 -
resultSetHoldability
,它的取值包括:ResultSet.HOLD_CURSORS_OVER_COMMIT
或ResultSet.CLOSE_CURSORS_AT_COMMIT
。
为了使得ResultSet能够对数据进行操作我们需要:
-
将resultSetType设置为
ResultSet.TYPE_SCROLL_SENSITIVE
。 -
将resultSetConcurrency设置为
ResultSet.CONCUR_UPDATABLE
。
在通过ResultSet对数据进行调整的过程中,下面方法可能会被调用:
- resultset.last()
- resultset.first()
- resultset.moveToInsertRow()
- resultset.absolute()
- resultset.setxxx()
- resultset.updateRow()
- resultset.insertRow()
下面是一个通过ResultSet对数据进行增、删、改的示例:
1 private static void getResultCount() throws SQLException 2 { 3 System.out.println("=====Result Count====="); 4 String dbURL = "jdbc:mysql://localhost/test"; 5 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 6 Statement st = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY, ResultSet.CLOSE_CURSORS_AT_COMMIT); 7 ResultSet rs = st.executeQuery("select * from user"); 8 rs.last(); 9 System.out.println("返回结果的条数:"+ rs.getRow()); 10 rs.first(); 11 12 rs.close(); 13 st.close(); 14 con.close(); 15 } 16 17 private static void insertDataToResultSet() throws SQLException 18 { 19 System.out.println("=====Insert====="); 20 String dbURL = "jdbc:mysql://localhost/test"; 21 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 22 Statement st = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); 23 ResultSet rs = st.executeQuery("select ID,NAME from user"); 24 rs.moveToInsertRow(); 25 rs.updateInt(1, 4); 26 rs.updateString(2, "Xiao Ming"); 27 rs.insertRow(); 28 showUser(st); 29 30 rs.close(); 31 st.close(); 32 con.close(); 33 } 34 35 private static void updateDataToResultSet() throws SQLException 36 { 37 System.out.println("=====Update====="); 38 String dbURL = "jdbc:mysql://localhost/test"; 39 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 40 Statement st = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE); 41 ResultSet rs = st.executeQuery("select * from user"); 42 rs.last(); 43 int count = rs.getRow(); 44 rs.first(); 45 rs.absolute(count); 46 rs.updateString(2, "Xiao Qiang"); 47 rs.updateRow(); 48 showUser(st); 49 50 rs.close(); 51 st.close(); 52 con.close(); 53 } 54 55 private static void delDataFromResultSet() throws SQLException 56 { 57 System.out.println("=====Delete====="); 58 String dbURL = "jdbc:mysql://localhost/test"; 59 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 60 Statement st = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE, ResultSet.CLOSE_CURSORS_AT_COMMIT); 61 ResultSet rs = st.executeQuery("select * from user"); 62 rs.last(); 63 int count = rs.getRow(); 64 rs.first(); 65 rs.absolute(count); 66 rs.deleteRow(); 67 showUser(st); 68 69 rs.close(); 70 st.close(); 71 con.close(); 72 }
分别调用上述方法:
1 getResultCount(); 2 insertDataToResultSet(); 3 updateDataToResultSet(); 4 delDataFromResultSet();
执行结果如下:
=====Result Count===== 返回结果的条数:2 =====Insert===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:4; NAME=Xiao Ming =====Update===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:4; NAME=Xiao Qiang =====Delete===== ID:1; NAME=Zhang San ID:2; NAME=TEST
可以看到我们对ID为4的记录进行了插入、更新和删除操作。
预处理以及批处理
预处理和批处理都是用来提升系统性能的方式,一种是利用数据库的缓存机制,一种是利用数据库一次执行多条语句的方式。
预处理
数据库服务器接收到Statement后,一般会解析Statement、分析是否有语法错误、定制最优的执行计划,这个过程可能会降低系统的性能。一般的数据库服务器都这对这种情况,设计了缓存机制,当数据库接收到指令时,如果缓存中已经存在,那么就不再解析,而是直接运行。
这里相同的指令是指sql语句完全一样,包括大小写。
JDBC使用PreparedStatement来完成预处理:
1 private static void test1() throws SQLException 2 { 3 System.out.println("=====Insert a single record by PreparedStatement====="); 4 String dbURL = "jdbc:mysql://localhost/test"; 5 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 6 PreparedStatement pst = con.prepareStatement("insert into user(id,name) values(?,?)"); 7 pst.setInt(1, 5); 8 pst.setString(2, "Lei Feng"); 9 pst.executeUpdate(); 10 showUser(pst); 11 pst.close(); 12 con.close(); 13 }
执行结果如下:
=====Insert a single record by PreparedStatement===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng
批处理
批处理是利用数据库一次执行多条语句的机制来提升性能,这样可以避免多次建立连接带来的性能损失。
批处理使用Statement的addBatch来添加指令,使用executeBatch方法来一次执行多条指令:
1 private static void test2() throws SQLException 2 { 3 System.out.println("=====Insert multiple records by Statement & Batch====="); 4 String dbURL = "jdbc:mysql://localhost/test"; 5 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 6 Statement st = con.createStatement(); 7 st.addBatch("insert into user(id,name) values(6,'Xiao Zhang')"); 8 st.addBatch("insert into user(id,name) values(7,'Xiao Liu')"); 9 st.addBatch("insert into user(id,name) values(8,'Xiao Zhao')"); 10 st.executeBatch(); 11 showUser(st); 12 st.close(); 13 con.close(); 14 }
执行结果如下:
=====Insert multiple records by Statement & Batch===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:6; NAME=Xiao Zhang ID:7; NAME=Xiao Liu ID:8; NAME=Xiao Zhao
预处理和批处理相结合
我们可以把预处理和批处理结合起来,利用数据库的缓存机制,一次执行多条语句:
1 private static void test3() throws SQLException 2 { 3 System.out.println("=====Insert multiple records by PreparedStatement & Batch====="); 4 String dbURL = "jdbc:mysql://localhost/test"; 5 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 6 PreparedStatement pst = con.prepareStatement("insert into user(id,name) values(?,?)"); 7 pst.setInt(1, 9); 8 pst.setString(2, "Xiao Zhang"); 9 pst.addBatch(); 10 pst.setInt(1, 10); 11 pst.setString(2, "Xiao Liu"); 12 pst.addBatch(); 13 pst.setInt(1, 11); 14 pst.setString(2, "Xiao Zhao"); 15 pst.addBatch(); 16 pst.executeBatch(); 17 showUser(pst); 18 pst.close(); 19 con.close(); 20 }
执行结果如下:
=====Insert multiple records by PreparedStatement & Batch===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:9; NAME=Xiao Zhang ID:10; NAME=Xiao Liu ID:11; NAME=Xiao Zhao
数据库事务
谈到数据库开发,事务是一个不可回避的话题,JDBC默认情况下,是每一步都自动提交的,我们可以通过设置connection.setAutoCommit(false)的方式来强制关闭自动提交,然后通过connection.commit()和connection.rollback()来实现事务提交和回滚。
简单的数据库事务
下面是一个简单的数据库事务的示例:
1 private static void transactionTest1() throws SQLException 2 { 3 System.out.println("=====Simple Transaction test====="); 4 String dbURL = "jdbc:mysql://localhost/test"; 5 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 6 Statement st = con.createStatement(); 7 try 8 { 9 con.setAutoCommit(false); 10 st.executeUpdate("insert into user(id,name) values(12, 'Xiao Li')"); 11 con.commit(); 12 } 13 catch(Exception ex) 14 { 15 ex.printStackTrace(); 16 con.rollback(); 17 } 18 finally 19 { 20 con.setAutoCommit(true); 21 showUser(st); 22 if (st != null) st.close(); 23 if (con != null) con.close(); 24 } 25 }
连续执行上述方法两次,我们可以得出下面的结果:
=====Simple Transaction test===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:12; NAME=Xiao Li =====Simple Transaction test===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:12; NAME=Xiao Li com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '12' for key 'PRIMARY' at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:931) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2870) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1573) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1665) at com.mysql.jdbc.Connection.execSQL(Connection.java:3170) at com.mysql.jdbc.Statement.executeUpdate(Statement.java:1316) at com.mysql.jdbc.Statement.executeUpdate(Statement.java:1235) at sample.jdbc.mysql.ResultSetSample.transactionTest1(ResultSetSample.java:154) at sample.jdbc.mysql.ResultSetSample.main(ResultSetSample.java:17)
可以看到,第一次调用时,操作成功,事务提交,向user表中插入了一条记录;第二次调用时,发生主键冲突异常,事务回滚。
带有SavePoint的事务
当我们的事务操作中包含多个处理,但我们有时希望一些操作完成后可以先提交,这样可以避免整个事务的回滚。JDBC使用SavePoint来实现这一点。
1 private static void transactionTest2() throws SQLException 2 { 3 System.out.println("=====Simple Transaction test====="); 4 String dbURL = "jdbc:mysql://localhost/test"; 5 Connection con = DriverManager.getConnection(dbURL, "root", "123"); 6 Statement st = con.createStatement(); 7 Savepoint svpt = null; 8 try 9 { 10 con.setAutoCommit(false); 11 st.executeUpdate("insert into user(id,name) values(13, 'Xiao Li')"); 12 st.executeUpdate("insert into user(id,name) values(14, 'Xiao Wang')"); 13 svpt = con.setSavepoint("roll back to here"); 14 st.executeUpdate("insert into user(id,name) values(15, 'Xiao Zhao')"); 15 st.executeUpdate("insert into user(id,name) values(13, 'Xiao Li')"); 16 con.commit(); 17 } 18 catch(Exception ex) 19 { 20 ex.printStackTrace(); 21 con.rollback(svpt); 22 } 23 finally 24 { 25 con.setAutoCommit(true); 26 showUser(st); 27 if (st != null) st.close(); 28 if (con != null) con.close(); 29 } 30 }
执行结果如下:
=====Simple Transaction test===== com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '13' for key 'PRIMARY' at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:931) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2870) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1573) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1665) at com.mysql.jdbc.Connection.execSQL(Connection.java:3170) at com.mysql.jdbc.Statement.executeUpdate(Statement.java:1316) at com.mysql.jdbc.Statement.executeUpdate(Statement.java:1235) at sample.jdbc.mysql.ResultSetSample.transactionTest2(ResultSetSample.java:185) at sample.jdbc.mysql.ResultSetSample.main(ResultSetSample.java:18) ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:13; NAME=Xiao Li ID:14; NAME=Xiao Wang
可以看到最终事务报出了主键冲突异常,事务回滚,但是依然向数据库中插入了ID为13和14的记录。
另外,在确定SavePoint后,ID为15的记录并没有被插入,它是通过事务进行了回滚。