JDBC基础入门(3)

阅读 68

2022-04-02

事务


事务是由一步/几步数据库操作序列组成的逻辑执行单元, 这些操作要么全部执行, 要么全部不执行.


注: 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时, 程序将会非正常退出,事务也会自动回滚;但如果程序捕获了该异常, 则需要在异常处理块中显式地回滚事务.

隔离级别


在相同数据环境下,使用相同输入,执行相同操作,根据不同的隔离级别,会导致不同的结果.不同的事务隔离级别能够解决的数据并发问题的能力是不同的, 由弱到强分为以下四级:

JDBC基础入门(3)_批处理

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

    }

}


精彩评论(0)

0 0 举报