JDBC基础入门(3)
事务
事务是由一步/几步数据库操作序列组成的逻辑执行单元, 这些操作要么全部执行, 要么全部不执行.
注: MySQL事务功能需要有InnoDB存储引擎的支持, 详见MySQL存储引擎InnoDB与Myisam的主要区别.
ACID特性
原子性(A: Atomicity): 事务是不可再分的最小逻辑执行体;
一致性(C: Consistency): 事务执行的结果, 必须使数据库从一个一致性状态, 变为另一个一致性状态.
隔离性(I: Isolation): 各个事务的执行互不干扰, 任意一个事务的内部操作对其他并发事务都是隔离的(并发执行的事务之间不能看到对方的中间状态,不能互相影响)
持续性(D: Durability): 持续性也称持久性(Persistence), 指事务一旦提交, 对数据所做的任何改变都要记录到永久存储器(通常指物理数据库).
Commit/Rollback
当事务所包含的全部操作都成功执行后提交事务,使操作永久生效,事务提交有两种方式:
1). 显式提交: 使用commit;
2). 自动提交: 执行DDL/DCL语句或程序正常退出;
当事务所包含的任意一个操作执行失败后应该回滚事务, 使该事务中所做的修改全部失效, 事务回滚也有两种方式:
1). 显式回滚: 使用rollback;
2). 自动回滚: 系统错误或强行退出.
注意: 同一事务中所有的操作,都必须使用同一个Connection.
JDBC支持
JDBC对事务的支持由Connection提供, Connection默认打开自动提交,即关闭事务,SQL语句一旦执行, 便会立即提交数据库,永久生效,无法对其进行回滚操作,因此需要关闭自动提交功能.
首先创建一张表用于测试
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`money` decimal(10,0) unsigned zerofill NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name_UNIQUE` (`name`),
UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=UTF8;
插入两条测试数据
INSERT INTO `account` (`name`, `money`) VALUES ('feiqing', '7800');
INSERT INTO `account` (`name`, `money`) VALUES ('xiaofang', '7800');
No Transaction
/**
* @author jifang
* @since 16/2/19 下午5:02.
*/
public class TransactionClient {
private Connection connection = ConnectionManger.getConnection("common.properties");
@Test
public void noTransaction() throws SQLException {
try (
PreparedStatement minusSM = connection.prepareStatement("UPDATE `account` SET `money`=(`money` - ?) WHERE `name`=?");
PreparedStatement addSM = connection.prepareStatement("UPDATE `account` SET `money`=(`money` + ?) WHERE `name`=?")
) {
// 从feiqing账户转出
minusSM.setBigDecimal(1, new BigDecimal(100));
minusSM.setString(2, "feiqing");
minusSM.execute();
// 中途抛出异常, 会导致两账户前后不一致
if (true){
throw new RuntimeException("no-transaction");
}
// 转入xiaofang账户
addSM.setBigDecimal(1, new BigDecimal(100));
addSM.setString(2, "xiaofang");
addSM.execute();
}
}
@After
public void tearDown() {
try {
connection.close();
} catch (SQLException e) {
}
}
}
By Transaction
@Test
public void byTransaction() throws SQLException {
boolean autoCommitFlag = connection.getAutoCommit();
// 关闭自动提交, 开启事务
connection.setAutoCommit(false);
try (
PreparedStatement minusSM = connection.prepareStatement("UPDATE `account` SET `money`=(`money` - ?) WHERE `name`=?");
PreparedStatement addSM = connection.prepareStatement("UPDATE `account` SET `money`=(`money` + ?) WHERE `name`=?")
) {
// 从feiqing账户转出
minusSM.setBigDecimal(1, new BigDecimal(100));
minusSM.setString(2, "feiqing");
minusSM.execute();
// 中途抛出异常: rollback
if (true) {
throw new RuntimeException("no-transaction");
}
// 转入xiaofang账户
addSM.setBigDecimal(1, new BigDecimal(100));
addSM.setString(2, "xiaofang");
addSM.execute();
connection.commit();
} catch (Throwable e) {
connection.rollback();
throw new RuntimeException(e);
} finally {
connection.setAutoCommit(autoCommitFlag);
}
}
注意: 当Connection遇到一个未处理的SQLException时, 程序将会非正常退出,事务也会自动回滚;但如果程序捕获了该异常, 则需要在异常处理块中显式地回滚事务.
隔离级别
在相同数据环境下,使用相同输入,执行相同操作,根据不同的隔离级别,会导致不同的结果.不同的事务隔离级别能够解决的数据并发问题的能力是不同的, 由弱到强分为以下四级:
MySQL设置事务隔离级别:
set session transaction isolation level [read uncommitted | read committed | repeatable read |serializable]
查看当前事务隔离级别:
select @@tx_isolation
JDBC设置隔离级别
connection.setTransactionIsolation(int level)
level可为以下值:
1). Connection.TRANSACTION_READ_UNCOMMITTED
2). Connection.TRANSACTION_READ_COMMITTED
3). Connection.TRANSACTION_REPEATABLE_READ
4). Connection.TRANSACTION_SERIALIZABLE
附: 事务并发读问题
1. 脏读(dirty read):读到另一个事务的未提交的数据,即读取到了脏数据(read commited级别可解决).
2. 不可重复读(unrepeatable read):对同一记录的两次读取不一致,因为另一事务对该记录做了修改(repeatable read级别可解决)
3. 幻读/虚读(phantom read):对同一张表的两次查询不一致,因为另一事务插入了一条记录(repeatable read级别可解决)
不可重复读和幻读的区别:
不可重复读是读取到了另一事务的更新;
幻读是读取到了另一事务的插入(MySQL中无法测试到幻读,效果与不可重复读一致);
其他关于并发事务问题可参考<数据库事务并发带来的问题>
批处理
多条SQL语句被当做同一批操作同时执行.
调用Statement对象的addBatch(String sql)方法将多条SQL语句收集起来, 然后调用executeBatch()同时执行.
为了让批量操作可以正确进行, 必须把批处理视为单个事务, 如果在执行过程中失败, 则让事务回滚到批处理开始前的状态.
public class SQLClient {
private Connection connection = null;
private Random random = new Random();
@Before
public void setUp() {
connection = ConnectionManger.getConnectionHikari("common.properties");
}
@Test
public void updateBatch() throws SQLException {
List<String> sqlList = Lists.newArrayListWithCapacity(10);
for (int i = 0; i < 10; ++i) {
sqlList.add("INSERT INTO user(name, password) VALUES('student" + i + "','" + encodeByMd5(random.nextInt() + "") + "')");
}
int[] results = update(connection, sqlList);
for (int result : results) {
System.out.printf("%d ", result);
}
}
private int[] update(Connection connection, List<String> sqlList) {
boolean autoCommitFlag = false;
try {
autoCommitFlag = connection.getAutoCommit();
// 关闭自动提交, 打开事务
connection.setAutoCommit(false);
// 收集SQL语句
Statement statement = connection.createStatement();
for (String sql : sqlList) {
statement.addBatch(sql);
}
// 批量执行 & 提交事务
int[] result = statement.executeBatch();
connection.commit();
return result;
} catch (SQLException e) {
try {
connection.rollback();
} catch (SQLException ignored) {
}
throw new RuntimeException(e);
} finally {
try {
connection.setAutoCommit(autoCommitFlag);
} catch (SQLException ignored) {
}
}
}
private String encodeByMd5(String input) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
BASE64Encoder base64Encoder = new BASE64Encoder();
return base64Encoder.encode(md5.digest(input.getBytes("utf-8")));
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@After
public void tearDown() {
try {
connection.close();
} catch (SQLException ignored) {
}
}
}
注:
1). 对于批处理,也可以使用PreparedStatement,建议使用Statement,因为PreparedStatement的预编译空间有限,当数据量过大时,可能会引起内存溢出.
2). MySQL默认也没有打开批处理功能,需要在URL中设置rewriteBatchedStatements=true参数打开.
DbUtils
commons-dbutils是Apache Commons组件中的一员,提供了对JDBC的简单封装,以简化JDBC编程;使用dbutils需要在pom.xml中添加如下依赖:
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
dbutils的常用类/接口如下:
DbUtils: 提供了一系列的实用静态方法(如:close());
ResultSetHandler: 提供对结果集ResultSet与JavaBean等的转换;
QueryRunner:
update()(执行insert/update/delete)
query()(执行select)
batch()(批处理).
QueryRunner更新
常用的update方法签名如下:
int update(String sql, Object... params);
int update(Connection conn, String sql, Object... params);
/**
* @author jifang
* @since 16/2/20 上午10:25.
*/
public class QueryRunnerClient {
@Test
public void update() throws SQLException {
QueryRunner runner = new QueryRunner(ConnectionManger.getDataSourceHikari("common.properties"));
String sql = "INSERT INTO t_ddl(username, password) VALUES(?, ?)";
runner.update(sql, "fq", "fq_password");
}
}
第二种方式需要提供Connection, 这样多次调用update可以共用一个Connection, 因此调用该方法可以支持事务;
QueryRunner查询
QueryRunner常用的query方法签名如下:
<T> T query(String sql, ResultSetHandler<T> rsh, Object... params);
<T> T query(Connection conn, String sql, ResultSetHandler<T> rsh, Object... params);
query()方法会通过sql语句和params参数查询出ResultSet,然后通过ResultSetHandler将ResultSet转换成对应的JavaBean返回.
public class QueryRunnerClient {
// ...
@Test
public void select() throws SQLException {
QueryRunner runner = new QueryRunner();
String sql = "SELECT * FROM t_ddl WHERE id = ?";
TDDL result = runner.query(ConnectionManger.getConnectionHikari("common.properties"), sql, rsh, 7);
System.out.println(result);
}
private ResultSetHandler<TDDL> rsh = new ResultSetHandler<TDDL>() {
@Override
public TDDL handle(ResultSet rs) throws SQLException {
TDDL tddl = new TDDL();
if (rs.next()) {
tddl.setId(rs.getInt(1));
tddl.setUsername(rs.getString(2));
tddl.setPassword(rs.getString(3));
}
return tddl;
}
};
private static class TDDL {
private Integer id;
private String username;
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "TDDL{" +
"id=" + id +
", username='" + username + ''' +
", password='" + password + ''' +
'}';
}
}
}
ResultSetHandler
在上例中, 我们使用自定的ResultSetHandler将ResultSet转换成JavaBean, 但实际上dbutils默认已经提供了很多定义良好的Handler实现:
BeanHandler : 单行处理器,将ResultSet转换成JavaBean;
BeanListHandler : 多行处理器,将ResultSet转换成List<JavaBean>;
MapHandler : 单行处理器,将ResultSet转换成Map<String,Object>, 列名为键;
MapListHandler : 多行处理器,将ResultSet转换成List<Map<String,Object>>;
ScalarHandler : 单行单列处理器,将ResultSet转换成Object(如保存SELECT COUNT(*) FROM t_ddl).
ColumnListHandler : 多行单列处理器,将ResultSet转换成List<Object>(使用时需要指定某一列的名称/编号,如new ColumListHandler(“name”):表示把name列数据放到List中);
public class QueryRunnerClient {
private QueryRunner runner = new QueryRunner(ConnectionManger.getDataSourceHikari("common.properties"));
@Test
public void clientBeanHandler() throws SQLException {
String sql = "SELECT * FROM t_ddl WHERE id = ?";
TDDL result = runner.query(sql, new BeanHandler<>(TDDL.class), 7);
System.out.println(result);
}
@Test
public void clientBeanListHandler() throws SQLException {
String sql = "SELECT * FROM t_ddl";
List<TDDL> result = runner.query(sql, new BeanListHandler<>(TDDL.class));
System.out.println(result);
}
@Test
public void clientScalarHandler() throws SQLException {
String sql = "SELECT COUNT(*) FROM t_ddl";
Long result = runner.query(sql, new ScalarHandler<Long>());
System.out.println(result);
}
@Test
public void clientColumnListHandler() throws SQLException {
String sql = "SELECT * FROM t_ddl";
List<String> query = runner.query(sql, new ColumnListHandler<String>("username"));
for (String i : query) {
System.out.printf("%n%s", i);
}
}
}
QueryRunner批处理
QueryRunner提供了批处理方法int[] batch(String sql, Object[][] params)(由于更新一行时需要Object[] param作为参数, 因此批处理需要指定Object[][] params,其中每个Object[]对应一条记录):
public class QueryRunnerClient {
private QueryRunner runner = new QueryRunner(ConnectionManger.getDataSourceHikari("common.properties"));
private Random random = new Random();
@Test
public void clientBeanHandler() throws SQLException {
String sql = "INSERT INTO t_ddl(username, password) VALUES(?, ?)";
int count = 46;
Object[][] params = new Object[count][];
for (int i = 0; i < count; ++i) {
params[i] = new Object[]{"student-" + i, "password-" + random.nextInt()};
}
runner.batch(sql, params);
}
}
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- python第十二课——for in循环
- Linux系统实战——批量无人值守安装操作系统
- python第十三课——嵌套循环
- python第十四课--排序及自定义函数
- python第十四课--排序及自定义函数之案例一:选择排序
- python第十四课--排序及自定义函数之案例二:冒泡排序
- python第十四课--排序及自定义函数之自定义函数(案例一)
- python第十四课--排序及自定义函数之自定义函数(案例二)
- python第十四课--排序及自定义函数之自定义函数(案例三)
- python第十四课--排序及自定义函数之自定义函数(案例四)
- python第十四课--排序及自定义函数之自定义函数(案例五)
- python第十五课——全局变量and局部变量
- python第十六课——ascii码
- python第十六课——外部函数and内部函数
- python第十七课——列表生成式