JDBC 入门 - 事务

Transaction(事务)

事务简介

事务的作用是保护数据完整性.它将一系列操作作为一个原子操作来处理, 从而防止多个操作中有的操作失败有的操作成功, 造成数据不完整. 在 JDBC 中, 一个连接默认的提交方式是 Auto-Commit 模式, 在这种模式下, 每个单独的SQL语句都会被视为一个事务, 当SQL执行时(更确切的说是完成时,详见 这里), 就会被提交到数据库. 然而, 在要求多个 SQL 同时成功或者失败的数据完整性问题时, 这种模式不能满足需要.

禁用 Auto-Commit模式

JDBC 为我们提供了手动管理事务的方式. 在取得 Connection 后, 调用:

conn.setAutoCommit(false);

 

就可以关闭自动提交, 同时, 这也开启了一个事务.

提交/回滚事务

当 Auto-Commit 禁用时, 我们需要自己管理事务, 提交事务的 API 为:

  conn.commit()

 

调用后, 事务内所有的 SQL 语句将会被视为一个原子被提交.

然而, 执行 SQL 的时候也有可能出现异常, 异常产生后, 我们需要保持数据完整性, 要回滚已经做的改变:

conn.rollback();

 

调用 conn.rollback() 的时机是在 catch 语句内, 因为当有SQL执行失败时, 会有SQLException抛出,这也是唯一知道是否成功提交与否的途径. 所以将 conn.rollback() 写在 catch 中吧.

Note: 已经 commit 的事务, 无法再 rollback.

事务隔离等级

利用事务,我们可以保证一系列操作的原子性, 但是却不能保证多个事务之间的影响. 考虑以下情况:

1) 当事务A在更新表的一行数据, 当执行更新后但是在提交事务前, 另外一个事务B来读走了数据, 这时事务A发现自己更新的数据有问题, 并没有做提交, 而是回滚了刚才的操作, 事务B实际上读到的是未提交的数据.

2) 当事务A读取了一个行数据, 之后事务B对这行数据进行了修改并提交, 这时事务A又重读了一次这行, 发现同一个事务内两次读到数据不一样.

3) 当事务A根据条件查询了若干行数据, 之后事务B插入/更新/删除了同一个表中的几行数据, 而事务B中修改的数据恰好有几条符合事务A的查询条件, 这时事务A由根据之前的条件重新查询, 发现同一个事务根据同一查询条件查到的结果却不同.

第一种现象叫 Dirty Read(脏读). 第二种现象叫 Repeatable Read(重复读). 第三种现象叫 Phantom Read(幻读).

解决这三种现象是数据库的锁机制, 比如行级锁, 表级锁, 但是各个数据库的实现方法不同. JDBC中我们可以控制事务的隔离级别, 也就是锁的各种机制, 来避免这三种现象. 相关API为:

    conn.setTransactionIsolation(/*...*/);

可以传入的隔离级别有:

Transaction Isolation Level

值得注意的是, 并不是隔离级别越高越好, 隔离级别越高, 数据越精确, 但是由于锁的范围也会相对变大, 数据多的话与数据库I/O的开销也会越大. 所以选择合适的隔离等级也是提升系统性能的关键之一.

对于事务的隔离级别, JDBC 并不是要求每种都支持的, 厂商可以有不同的方式以及实现情况, 可以调用 DatabaseMetaData.supportsTransactionIsolationLevel() 来查看支持情况. 调用 DatabaseMetaData.getDefaultTransactionIsolation 可以得到默认的隔离级别.

以下代码示例如何获取隔离级别的支持情况:

    public void printTransactionIsolation() throws SQLException {
      Connection conn = getConnection();
      DatabaseMetaData md = conn.getMetaData();
      System.out.println("TRANSACTION NONE: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_NONE));
      System.out.println("TRANSACTION READ COMMITTED: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_COMMITTED));
      System.out.println("TRANSACTION READ UNCOMMITTED: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
      System.out.println("TRANSACTION REPEATABLE READ: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_REPEATABLE_READ));
      System.out.println("TRANSACTION SERIALIZABLE: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_SERIALIZABLE));
      System.out.print("Default Transaction: ");
      switch(md.getDefaultTransactionIsolation()) {
          case Connection.TRANSACTION_NONE:
              System.out.println("None");
              break;
          case Connection.TRANSACTION_READ_COMMITTED:
              System.out.println("Read Commit");
              break;
          case Connection.TRANSACTION_READ_UNCOMMITTED:
              System.out.println("Read Uncommit");
              break;
          case Connection.TRANSACTION_REPEATABLE_READ:
              System.out.println("Repeatable read");
              break;
          case Connection.TRANSACTION_SERIALIZABLE:
              System.out.println("Serializable");
              break;
      }
      conn.close();
  }

 

Mysql 和 JavaDB 的输出结果都为: 

TRANSACTION NONE: false
TRANSACTION READ COMMITTED: true
TRANSACTION READ UNCOMMITTED: true
TRANSACTION REPEATABLE READ: true
TRANSACTION SERIALIZABLE: true
Default Transaction: Read Commit

 

Savepoint 保存点

JDBC 3.0 引入了 Savepoint 接口. 我们可以在代码中设立 Savepoint 然后根据条件 rollback 到某个点, 这样的好处是可以更灵活的控制事务的提交内容. 创建检查点的API为:

  Savepoint svpt1 = conn.setSavepoint();

 

回滚到某一检查点的 API 为:

 conn.rollback(svpt1);

 

释放某个检查点的 API 为:

conn.releaseSavepoint(svpt1);

 

Note: 当整个事务提交或回滚时, 事务内的 Savepoint 会自动失效. 当事务回滚到某个 Savepoint 时, 之后创建的 Savepoint 也会释放.

 

最后附上一个完整的例子,这个例子将一个文章(post)插入的 POST 表, 将文章的标签(tag)插入到 TAG 表, 然后将用一张中间表(POST_TAG)将两者关联起来:

public class Transaction {

    private static final String INSERT_POST
            = "INSERT INTO POSTS VALUES(NULL, ?, ?, DEFAULT, ?)";

    private static final String INSERT_TAG
            = "INSERT IGNORE INTO TAGS VALUES(NULL, ?)";

    private static final String INSERT_POST_TAG
            = "INSERT INTO POST_TAG VALUES(NULL, ?, ?)";

    private Properties dbProps = new Properties();
    private Transaction() {}

    public Properties getDbProps() {
        return dbProps;
    }

    public void setDbProps(Properties dbProps) {
        this.dbProps = dbProps;
    }

    public void insertPost(Post post) {

        Connection conn = null;
        try {
            conn = DBUtils.getConnection(dbProps);
            conn.setAutoCommit(false);

            PreparedStatement insertTag = conn.prepareStatement(INSERT_TAG);
            Statement queryTags = conn.createStatement();
            PreparedStatement insertPost = conn.prepareStatement(INSERT_POST, Statement.RETURN_GENERATED_KEYS);
            PreparedStatement insertPostTag = conn.prepareStatement(INSERT_POST_TAG);

            ArrayList<Tag> tags = post.getTagList();
            ArrayList<String> tagNameList = new ArrayList<String>();
            for (Tag t : tags) {
                tagNameList.add(t.getName());
                insertTag.setString(1, t.getName());
                insertTag.addBatch();
            }
            insertTag.executeBatch();
            // 更好的方法是存储过程,MySQL不支持 ANY(?)
            StringBuilder sqlbuilder = new StringBuilder();
            for (Tag t : tags) {
                sqlbuilder.append("\"" + t.getName() + "\"" +",");
            }
            String sqlQueryTags = "SELECT * FROM TAGS WHERE NAME IN ("
                    + sqlbuilder.deleteCharAt(sqlbuilder.length()-1) + ")";

            if (tags.size() > 0 && !queryTags.execute(sqlQueryTags)) {
                throw new SQLException("Query tags failed.");
            } else {
                ResultSet rsTags = queryTags.getResultSet();
                ArrayList<Integer> tagIDList = new ArrayList<Integer>();
                while (rsTags.next()) {
                    tagIDList.add(rsTags.getInt("ID"));
                }
                rsTags.close();

                insertPost.setString(1, post.getTitle());
                insertPost.setString(2, post.getContent());
                insertPost.setBoolean(3, post.isVisible());
                insertPost.executeUpdate();
                ResultSet generatedKeys = insertPost.getGeneratedKeys();
                if (generatedKeys.next()) {
                    post.setId(generatedKeys.getInt(1));
                    System.out.println("Generated new key for post : " + generatedKeys.getInt(1));
                } else {
                    throw new SQLException("Creating user failed, no ID obtained");
                }

                for (int i : tagIDList) {
                    insertPostTag.setInt(1, post.getId());
                    insertPostTag.setInt(2, i);
                    insertPostTag.addBatch();
                }
                insertPostTag.executeBatch();
                conn.commit();
            }
        } catch (SQLException e) {
            DBUtils.printSQLException(e);
            try {
                if (conn != null) {
                    conn.rollback();
                }
            } catch (SQLException e1) {
                DBUtils.printSQLException(e1);
            }
        } finally {
            try {
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException e2) {
                DBUtils.printSQLException(e2);
            }
        }
    }

    public static void main(String [] args) throws IOException, SQLException {
        Properties props = new Properties();
        props.load(ClassLoader.getSystemResourceAsStream("db.mysql.props"));
        Connection conn = DBUtils.getConnection(props);
        DBUtils.printTransactionIsolation(conn);

        Transaction trans = new Transaction();
        trans.setDbProps(props);

        Post post = new Post();
        post.setTitle("JDBC");
        post.setContent("JDBC Introduction");

        ArrayList<Tag> tagList = new ArrayList<Tag>();
        tagList.add(new Tag("JDBC"));
        tagList.add(new Tag("Java"));
        post.setTagList(tagList);
        trans.insertPost(post);
    }
}
Transaction

工具类 DBUtils:

public class DBUtils {
    public static Connection getConnection(Properties props) throws SQLException {
        String url = props.getProperty("url");
        String user = props.getProperty("user");
        Connection conn = null;
        if (user.length() == 0) {
            conn = DriverManager.getConnection(url);
        } else {
            conn = DriverManager.getConnection(url, props);
        }
        String dbName = props.getProperty("dbName");
        conn.setCatalog(dbName);
        return conn;
    }

    public static void printWarnings(SQLWarning warning)
            throws SQLException {
        while (warning != null) {
            System.out.println("Message: " + warning.getMessage());
            System.out.println("SQLState: " + warning.getSQLState());
            System.out.print("Vendor error code: ");
            System.out.println(warning.getErrorCode());
            System.out.println("");
            warning = warning.getNextWarning();
        }
    }

    public static void printSQLException(SQLException ex) {
        for (Throwable e : ex) {    // Iterator 会调用 getNextException()
            if (e instanceof SQLException) {

                e.printStackTrace(System.err);
                System.err.println("SQLState: " +
                        ((SQLException)e).getSQLState());

                System.err.println("Error Code: " +
                        ((SQLException)e).getErrorCode());

                System.err.println("Message: " + e.getMessage());

                Throwable t = ex.getCause();
                while(t != null) {  // 打印每个 cause
                    System.out.println("Cause: " + t);
                    t = t.getCause();
                }
            }
        }
    }

    public static void printTransactionIsolation(Connection conn) throws SQLException {
        DatabaseMetaData md = conn.getMetaData();
        System.out.println("TRANSACTION NONE: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_NONE));
        System.out.println("TRANSACTION READ COMMITTED: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_COMMITTED));
        System.out.println("TRANSACTION READ UNCOMMITTED: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
        System.out.println("TRANSACTION REPEATABLE READ: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_REPEATABLE_READ));
        System.out.println("TRANSACTION SERIALIZABLE: " + md.supportsTransactionIsolationLevel(Connection.TRANSACTION_SERIALIZABLE));
        System.out.print("Default Transaction: ");
        switch(md.getDefaultTransactionIsolation()) {
            case Connection.TRANSACTION_NONE:
                System.out.println("None");
                break;
             case Connection.TRANSACTION_READ_COMMITTED:
                System.out.println("Read Commit");
                break;
            case Connection.TRANSACTION_READ_UNCOMMITTED:
                System.out.println("Read Uncommit");
                break;
            case Connection.TRANSACTION_REPEATABLE_READ:
                System.out.println("Repeatable read");
                break;
             case Connection.TRANSACTION_SERIALIZABLE:
                System.out.println("Serializable");
                break;
            }
        conn.close();
    }
}
DBUtils

MYSQL 建表代码:

CREATE DATABASE IF NOT EXISTS testdb;
USE testdb;

CREATE TABLE IF NOT EXISTS TAGS (
ID INT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(255) UNIQUE NOT NULL,
PRIMARY KEY(ID)
);

CREATE TABLE IF NOT EXISTS POSTS (
ID INT NOT NULL AUTO_INCREMENT, 
TITLE VARCHAR(255) NOT NULL, 
CONTENT TEXT NOT NULL, 
DT_CREATE DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 
VISIBLE BOOLEAN DEFAULT TRUE, 
PRIMARY KEY(ID)
);

CREATE TABLE IF NOT EXISTS POST_TAG (
ID INT NOT NULL AUTO_INCREMENT, 
PID INT NOT NULL, 
TID INT NOT NULL, 
PRIMARY KEY(ID), 
FOREIGN KEY(PID) 
REFERENCES POSTS(ID), 
FOREIGN KEY(TID) 
REFERENCES TAGS(ID)
);
SQL

 

posted @ 2015-01-25 22:09  still_water  阅读(156)  评论(0)    收藏  举报