JDBC基础入门(3)

时间:2022-05-04
本文章向大家介绍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);
    }
}