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);
    }
}

原文发布于微信公众号 - Java帮帮(javahelp)

原文发表时间:2016-12-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏架构师之旅

Oracle使用总结之异常篇

1.1 异常处理概念 1.1.1 预定义的异常处理 1.1.2 非预定义的异常处理 1.1.3 用户自定义的异常处理 1.1.4 用户定义的异常处理 1.2 ...

2206
来自专栏Java3y

移动商城第五篇(用户模块)【用户登陆、回显用户、拦截器、收货地址】

移动商城【用户登陆、回显用户】 我们来实现用户登陆的功能: ? 当点击的时候,出来的是一个弹出框,我们想要切换成一个页面。 ? 找到对应的事件、切换成我们的页面...

5057
来自专栏MasiMaro 的技术博文

驱动开发中的常用操作

这篇文章会持续更新,由于在驱动中,有许多常用的操作代码几乎不变,而我自己有时候长时间不用经常忘记,所以希望在这把一些常用的操作记录下来,当自己遗忘的时候,有个参...

1764
来自专栏Hongten

java file 文件操作 operate file of java

1152
来自专栏恰童鞋骚年

Hadoop学习笔记—9.Partitioner与自定义Partitioner

  在第四篇博文《初识MapReduce》中,我们认识了MapReduce的八大步凑,其中在Map阶段总共五个步骤,如下图所示:

722
来自专栏跟着阿笨一起玩NET

单件模式Singleton来控制窗体被重复或多次打开

本文转载:http://blog.csdn.net/a0700746/article/details/4473796

722
来自专栏web编程技术分享

【手把手】JavaWeb 入门级项目实战 -- 文章发布系统 (第十一节)1.根据ID查询文章数据2.评论功能后台业务实现

8194
来自专栏Jerry的SAP技术分享

使用ABAP正则表达式解析HTML标签

需求就是我用ABAP的某个函数从数据库读取一个字符串出来,该字符串的内容是一个网页。

1282
来自专栏恰童鞋骚年

《T-SQL查询》读书笔记Part 3.索引的基本知识

索引优化是查询优化中最重要的一部分,索引是一种用于排序和搜索的结构,在查找数据时索引可以减少对I/O的需要;当计划中的某些元素需要或是可以利用经过排序的数据时,...

1263
来自专栏Java帮帮-微信公众号-技术文章全总结

高级框架-springDate-JPA 第二天【悟空教程】

通过annotation(注解)来映射实体类和数据库表的对应关系,基于annotation的主键标识为@Id注解, 其生成规则由@GeneratedValue ...

1921

扫码关注云+社区

领取腾讯云代金券