阅读本文需要的先修知识:
- 最基本的SQL语句
- 最基本的JDBC操作(如插入单条记录)
如急需使用请直接看最后一段代码。
在JDBC中,对记录进行修改操作最简单的方法是使用executeUpdate()
方法,但该方法中的参数只能是单条SQL语句,其实对于需要一次执行多条语句的情况,JDBC也提供了批处理的机制。
1.事务
批处理基于事务处理,JDBC提供了两个方法void commit()
和void rollback()
,这两个函数的用法正如大部分SQL数据库中提供的事务处理语句一样,commit()
方法用来提交多条语句,rollback()
方法用来回滚至执行本次事务之前的状态。
请看如下代码:
public static void main(String[] args) {
Connection conn;
Statement stmt;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
//DB_URL,USER,PASS均为事先定义好的字符串,分别代表数据库地址,登录用户名,密码
stmt = conn.createStatement();
conn.setAutoCommit(false);
stmt.executeUpdate("INSERT INTO test(id, name, tel) VALUES(1, 'Chandler', '1111111')");
stmt.executeUpdate("INSERT INTO test(id, name, tel) VALUES(2, 'Joey', '2222222')");
stmt.executeUpdate("INSERT INTO test(id, name, tel) VALUES(3, 'Rachel', '3333333')");
conn.commit();
stmt.executeUpdate("INSERT INTO test(id, name, tel) VALUES(4, 'Monica', '4444444')");
stmt.executeUpdate("INSERT INTO test(id, name, tel) VALUES(5, 'Ross', '5555555')");
stmt.executeUpdate("INSERT INTO test(id, name, tel) VALUES(6, 'Phoebe', '666666')");
stmt.close();
conn.close();
} catch (SQLException se) {
se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
在执行命令之前,我们首先调用了一个参数为false
的setAutoCommit()
方法,这个方法的作用是使得在其之后执行的命令不会立即被提交给数据库,而是等到下一次调用commit()
方法时,才一次性全部提交。
在上面的代码中,我们首先执行了3条插入语句,然后进行了一次提交,之后又执行了3条插入语句,不同的是这3条插入语句执行后没有进行提交。执行这个程序之后,结果是这样的。
mysql> select * from test;
+----+----------+---------+
| id | name | tel |
+----+----------+---------+
| 1 | Chandler | 1111111 |
| 2 | Joey | 2222222 |
| 3 | Rachel | 3333333 |
+----+----------+---------+
3 rows in set (0.00 sec)
可见后面3条插入命令因为还没有commit
,所以是没有生效的。就这个结果来看,利用这样的方法,我们就可以先执行多条插入语句,再进行一次commit,达到一次插入多条记录的效果。但事实上,这样和不使用事务没有太大的区别,性能也没有什么提高,真正要实现批量插入,我们还需要借助JDBC的批处理机制。
2.批处理
在这里我们主要需要使用两个方法,分别是void addBatch(String command)
和int[] executeBatch()
。我们通过对上面的代码做一些改动来探究这两个方法的用法。
public static void main(String[] args) {
Connection conn;
Statement stmt;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
//DB_URL,USER,PASS均为事先定义好的字符串,分别代表数据库地址,登录用户名,密码
stmt = conn.createStatement();
conn.setAutoCommit(false);
stmt.addBatch("INSERT INTO test(id, name, tel) VALUES(1, 'Chandler', '1111111')");
stmt.addBatch("INSERT INTO test(id, name, tel) VALUES(2, 'Joey', '2222222')");
stmt.addBatch("INSERT INTO test(id, name, tel) VALUES(3, 'Rachel', '3333333')");
stmt.addBatch("INSERT INTO test(id, name, tel) VALUES(4, 'Monica', '4444444')");
stmt.addBatch("INSERT INTO test(id, name, tel) VALUES(5, 'Ross', '5555555')");
stmt.addBatch("INSERT INTO test(id, name, tel) VALUES(6, 'Phoebe', '666666')");
int[] counts = stmt.executeBatch(); //执行Batch中的全部语句
conn.commit(); //提交到数据库
for (int i : counts) {
if (i == 0) {
conn.rollback();
}
}
conn.setAutoCommit(true); //在完成批量操作后恢复默认的自动提交方式,提高程序的可扩展性
stmt.close();
conn.close();
} catch (SQLException se) {
se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
addBatch()
方法每调用一次,就相当于往一个假想的“批处理”中添加了一条语句,这些语句在下一次调用 executeBatch()
方法时一次性全部执行,在此之后,我们再次调用一个commit()
将所作的更改提交到数据库。executeBatch()
方法的返回值是一个int数组,里面保存了本次执行的每条语句的返回值,即受到影响的记录的行数,在本例中,数组中的所有值均应为1,如果为0则说明插入失败,我们可以选择进行回滚或者报错。
3.预备语句
每次调用addBatch()
方法时都需要输入一长串SQL语句显得十分繁琐,在操作列数比较多的表时就更是如此,为了避免这样的情况,我们可以使用预备语句。
预备语句的用法有点类似于printf()
的用法,当我们使用printf进行输出时,往往会在字符串中插入几个像%d
、%c
这样的占位符,至于这些位置具体的值,我们则在字符串后面再专门指定。
预备语句的占位符没有按类型进行区分,只有一种——?
,请看如下代码:
PreparedStatement pstm = conn.prepareStatement("INSERT INTO test(id, name, tel) VALUES(?, ?, ?")
pstm.setInt(1, 1);
pstm.setString(2, 'Chandler');
pstm.setString(3, '1111111');
pstm.executeUpdate();
首先我们使用带占位符?
的SQL语句初始化一个PreparedStatement
对象,然后分别使用setInt()
方法和setString()
方法给对应的位置填值,除了这两种方法还有很多其他类型的赋值方法,具体可以查阅官方文档或者利用IDE的自动补全功能进行查看,这一类方法的参数都是类似的,第一个参数指明要给第几个?
进行赋值,第二个参数要赋的;在给所有的位置赋值之后,我们调用executeUpdate()
方法执行这条语句。
上面代码的功能和下面的等同:
Statement stmt = conn.createStatement();
stmt.executeUpdate("INSERT INTO test(id, name, tel) VALUES(1, 'Chandler', '1111111')");
表面看来下面使用普通语句的方法更简洁,但当我们要操作的记录数变多,乃至成千上万条时,预备语句的优势就会体现出来。最后,我们将一开始的程序使用预备语句+批量更改+事务重写一遍:
public static void main(String[] args) {
Connection conn;
PreparedStatement pstm;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
//DB_URL,USER,PASS均为事先定义好的字符串,分别代表数据库地址,登录用户名,密码
conn.setAutoCommit(false);
pstm = conn.prepareStatement("INSERT INTO test(id, name, tel) VALUES(?, ?, ?)");
pstm.setInt(1, 1);
pstm.setString(2, "Chandler");
pstm.setString(3, "1111111");
pstm.addBatch();
pstm.setInt(1, 2);
pstm.setString(2, "Joey");
pstm.setString(3, "2222222");
pstm.addBatch();
pstm.setInt(1, 3);
pstm.setString(2, "Rachel");
pstm.setString(3, "3333333");
pstm.addBatch();
pstm.setInt(1, 4);
pstm.setString(2, "Monica");
pstm.setString(3, "4444444");
pstm.addBatch();
pstm.setInt(1, 5);
pstm.setString(2, "Ross");
pstm.setString(3, "5555555");
pstm.addBatch();
pstm.setInt(1, 6);
pstm.setString(2, "Phoebe");
pstm.setString(3, "666666");
pstm.addBatch();
int[] counts = pstm.executeBatch(); //执行Batch中的全部语句
conn.commit(); //提交到数据库
for (int i : counts) {
if (i == 0) {
conn.rollback();
}
}
conn.setAutoCommit(true); //在完成批量操作后恢复默认的自动提交方式,提高程序的可扩展性
pstm.close();
conn.close();
} catch (SQLException se) {
se.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
4. 总结
虽然在本文中我们举的例子是一次性插入六条数据,但我们更应该利用JDBC的批处理机制去执行一些更复杂的操作,比如WHERE条件不同的批量UPDATE操作,或者需要和for循环配合使用动态修改SQL语句的情况,等等。
参考文献
- Java核心技术·卷2:高级特性(原书第9版)(截止我写这篇文章时,已经出到第10版)
- MySQL必知必会
实验所用环境
- Windows 10(1809)
- jdk 1.8.0_101
- MySQL Sserver 8.0.13 for Win64 on x86_64
欢迎提出建议或意见
原创文章,转载请注明出处
2018-12-14更新:
感谢@风中的雪糕 和@p712long 指出问题,对文章做了少量修改。