MySQL數據庫與JDBC編程(四)

Java 7的RowSet 1.1

RowSet接口繼承了ResultSet接口,RowSet接口下包含JdbcRowSet、CachedRowSet、FilterRowSet、JoinRowSet和WebRowSet常用子接口。除了JdbcRowSet需要保持與數據庫的連接之外,其餘4個子接口都是離線的RowSet,無須保持與數據庫的連接。

與ResultSet相比,RowSet默認是可滾動、可更新、可序列化的結果集,而且作爲JavaBean使用,因此能方便地在網絡上傳輸,用於同步兩端的數據。對於離線RowSet而言,程序在創建RowSet時已把數據從底層數據庫讀取到了內存,因此可以充分利用計算機的內存,從而降低數據庫服務器的負載,提高程序性能。

Java 7新增的RowSetFactory與RowSet

Java7新增了RowSetProvider類和RowSetFactory接口,其中RowSetProvider負責創建RowSetFactory,而RowSetFactory則提供瞭如下方法來創建RowSet實例。

創建一個默認的CachedRowSet

public CachedRowSet createCachedRowSet() throws SQLException;

創建一個默認的FilteredRowSet

public FilteredRowSet createFilteredRowSet() throws SQLException;

創建一個默認的JdbcRowSet

public  JdbcRowSet createJdbcRowSet() throws SQLException;

創建一個默認的JoinRowSet

public  JoinRowSet createJoinRowSet() throws SQLException;

創建一個默認的WebRowSet

public  WebRowSet createWebRowSet() throws SQLException;

通過使用RowSetFactory,就可以把應用程序與RowSet實現類分離開,避免直接使用JdbcRowSetImpl等非公開的API,也更有利於後期的升級、擴展。

代碼演示

public class RowSetFactoryTest {

    private String driver;
    private String url;
    private String user;
    private String password;

    public void initParam(String paramFile) throws Exception {
        //使用Properties類來加載屬性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        password = props.getProperty("password");
    }

    public void update(String sql) throws Exception{
        Class.forName(driver);
        //使用RowSetProvider創建RowSetFactory
        RowSetFactory factory = RowSetProvider.newFactory();

        try (
                //創建默認的JdbcRowSet實例
                JdbcRowSet jdbcRs = factory.createJdbcRowSet()
                ){

            //設置必要的連接信息
            jdbcRs.setUrl(url);
            jdbcRs.setUsername(user);
            jdbcRs.setPassword(password);

            //設置SQL查詢語句
            jdbcRs.setCommand(sql);
            //執行查詢
            jdbcRs.execute();

            jdbcRs.afterLast();
            while (jdbcRs.previous()){
                System.out.println(jdbcRs.getString(1)
                        + "\t" + jdbcRs.getString(2)
                        + "\t" + jdbcRs.getString(3));

                if (jdbcRs.getInt("student_id") == 3){
                    //修改指定記錄行
                    jdbcRs.updateString("student_name", "孫悟空");
                    jdbcRs.updateRow();
                }

            }


        }


    }


    public static void main(String[] args) throws Exception{
        RowSetFactoryTest rt = new RowSetFactoryTest();
        rt.initParam("mysql.ini");
        rt.update("select * from student_table");
    }



}

離線RowSet

在使用ResultSet時代,程序查詢得到ResultSet之後必須立即讀取或處理它對應的記錄,否則一旦Connection關閉,再去通過ResultSet讀取記錄就會引發異常。在這種模式下,JDBC編程十分痛苦——假設應用程序被分爲兩層:數據訪問層和視圖顯示層,當應用程序在數據訪問層查詢得到ResultSet之後,對ResultSet的處理有如下兩種常見方式。

  1. 使用迭代訪問ResultSet裏的記錄,並將這些記錄轉換成Java Bean,再將多個Java Bean封裝成一個List集合,也就是完成ResultSet——>Java Bean集合的轉換。轉換完成後可以關閉Connection等資源,然後將Java Bean集合傳到視圖顯示層,視圖顯示層可以顯示查詢得到的數據。
  2. 直接將ResultSet傳到視圖顯示層——這要求當視圖顯示層顯示數據時,底層Connection必須一直處於打開狀態,否則ResultSet無法讀取記錄。

第一種方式比較安全,但編程十分繁瑣;第二種方式則需要Connection一直處於打開狀態,這不僅不安全,而且對程序性能也有較大的影響。

通過使用離線RowSet可以十分優雅地處理上面的問題,離線RowSet會直接將底層數據讀入內存中,封裝成RowSet對象,而RowSet對象則完全可以當成Java Bean來使用。因此不僅安全,而且編程十分簡單。

代碼演示

public class CachedRowSetTest {
    private static String driver;
    private static String url;
    private static String user;
    private static String password;

    public void initParam(String paramFile) throws Exception {
        //使用Properties類來加載屬性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        password = props.getProperty("password");
    }

    public CachedRowSet query(String sql) throws Exception{
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url, user, password);
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(sql);
        //使用RowSetProvider創建RowSetFactory
        RowSetFactory factory = RowSetProvider.newFactory();
        //創建默認的CachedRowSet實例
        CachedRowSet cachedRs = factory.createCachedRowSet();
        //使用ResultSet裝填RowSet
        cachedRs.populate(rs);

        //關閉資源
        rs.close();
        stmt.close();
        conn.close();
        return cachedRs;

    }

    public static void main(String[] args) throws Exception{
        CachedRowSetTest ct = new CachedRowSetTest();
        ct.initParam("mysql.ini");
        CachedRowSet rs = ct.query("select * from student_table");

        rs.afterLast();
        //向前滾動結果集
        while(rs.previous()){
            System.out.println(rs.getString(1)
                    + "\t" + rs.getString(2)
                    + "\t" + rs.getString(3));

            if (rs.getInt("student_id") == 3){
                //修改指定記錄行
                rs.updateString("student_name", "豬八戒");
                rs.updateRow();
            }
        }

        //重新獲取數據庫連接
        Connection conn = DriverManager.getConnection(url, user, password);
        conn.setAutoCommit(false);
        //把對RowSet所做的修改同步到底層數據庫
        rs.acceptChanges(conn);


    }


}

離線RowSet的查詢分頁

由於CachedRowSet會將數據記錄直接裝在到內存中,因此如果SQL查詢返回的記錄過大,CachedRowSet將會佔用大量的內存,在某些極端的情況下,它甚至會直接導致內存溢出。

爲了解決該問題,CachedRowset提供了分頁功能。所謂分頁功能就是一次只裝載ResultSet裏的某幾條記錄,這樣就可以避免CachedRowSet佔用內存過大的問題。

代碼演示

public class CachedRowSetPage {
    private String driver;
    private String url;
    private String user;
    private String password;

    public void initParam(String paramFile) throws Exception {
        //使用Properties類來加載屬性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        password = props.getProperty("password");
    }

    public CachedRowSet query(String sql, int pageSize, int page) throws Exception{
        Class.forName(driver);
        try (
                Connection conn = DriverManager.getConnection(url, user, password);
                Statement stmt = conn.createStatement();
                ResultSet rs = stmt.executeQuery(sql)
                ){

            RowSetFactory factory = RowSetProvider.newFactory();
            CachedRowSet cachedRs = factory.createCachedRowSet();
            //設置每頁顯示pageSize條記錄
            cachedRs.setPageSize(pageSize);
            //使用ResultSet裝填RowSet,設置從第幾條記錄開始
            cachedRs.populate(rs, (page - 1) * pageSize + 1);

            return cachedRs;

        }
    }

    public static void main(String[] args) throws Exception{
        CachedRowSetPage cp = new CachedRowSetPage();
        cp.initParam("mysql.ini");
        CachedRowSet rs = cp.query("select * from student_table", 3, 2);

        //向後滾動結果集
        while(rs.next()){
            System.out.println(rs.getString(1)
                    + "\t" + rs.getString(2)
                    + "\t" + rs.getString(3));
        }

    }



}

事務處理

事務的概念和MySQL事務支持

事務是由一步或幾步數據庫操作序列組成的邏輯執行單元,這系列操作要麼全部執行,要麼全部放棄執行。程序和事務是兩個不同的概念。一般而言,一段程序中可能包含多個事務。

事務具備4個特性

  1. 原子性(Atomicity):事務是應用中最小的執行單位,就如原子是自然界的最小顆粒,具有不可再分的特徵一樣,事務是應用中不可再分的最小邏輯執行體。
  2. 一致性(Consistency):事務執行的結果,必須使數據庫從一個一致性狀態,變到另一個一致性狀態。
  3. 隔離性(Isolation):各個事務的執行互不干擾,任意一個事務的內部操作對其他併發的事務都是隔離的,也就是說,併發執行的事務之間不能看到對方的中間狀態,併發執行的事務之間不能互相影響。
  4. 持續性(Durability):持續性也稱爲持久性,指事務一旦提交,對數據所做的任何改變都要記錄到永久存儲器中,通常就是保存進物理數據庫。

這4個特性也簡稱爲ACID性

數據庫的事務由下列語句組成。

  • 一組DML語句,經過這組DML語句修改後的數據將保持較好的一致性
  • 一條DDL語句
  • 一條DCL語句

DDL和DCL語句最多隻能有一條,因爲DDL和DCL語句都會導致事務立即提交。

當事務所包含的全部數據庫操作都成功執行後,應該提交事務,使這些修改永久生效。事務提交有兩種方式:顯式提交和自動提交。

顯式提交:使用commit。
自動提交:執行DCL或DDL語句,或者程序正常退出。

當事務所包含的任意一個數據庫操作執行失敗後,應該回滾事務,使該事務中所做的修改全部失效。事務回滾有兩種方式:顯式回滾和自動回滾。

顯式回滾:使用rollback。
自動回滾:系統錯誤或者強行退出。

MySQL默認關閉事務(即打開自動提交),在默認情況下,用戶在MySQL控制檯輸入一條DML語句,這條DML語句將會立即保存到數據庫裏。爲了開啓MySQL的事務支持,可以顯式調用如下命令

##  0爲關閉自動提交,即開啓事務
SET AUTOCOMMIT = {0 | 1}

自動提交和開啓事務恰好相反,如果開啓自動提交就是關閉事務;關閉自動提交就是開啓事務。

如果只是想臨時性地開始事務,則可以使用MySQL提供的start transaction或begin兩個命令,它們都表示臨時性地開始一次事務,處於start transaction或begin後的DML語句不會立即生效,除非使用commit顯式提交事務,或者執行DDL、DCL語句來隱式提交事務。

JDBC的事務支持

JDBC連接也提供了事務支持,JDBC連接的事務支持由Connection提供,Connection默認打開自動提交,即關閉事務,在這種情況下,每條SQL語句一旦執行,便會立即提交到數據庫,永久生效,無法對其進行回滾操作。

可以調用Connection的setAutoCommit()方法來關閉自動提交。

開啓事務

void setAutoCommit(boolean autoCommit) throws SQLException;

提交事務

void commit() throws SQLException;

回滾事務

void rollback() throws SQLException;

實際上,當Connection遇到一個未處理的SQLException異常時,系統將會非正常退出,事務也會自動回滾。但如果程序捕獲了該異常,則需要在異常處理塊中顯式地回滾事務。

代碼演示

public class TransactionTest {
    private String driver;
    private String url;
    private String user;
    private String password;

    public void initParam(String paramFile) throws Exception {
        //使用Properties類來加載屬性文件
        Properties props = new Properties();
        props.load(new FileInputStream(paramFile));
        driver = props.getProperty("driver");
        url = props.getProperty("url");
        user = props.getProperty("user");
        password = props.getProperty("password");
    }

    public void insertInTransaction(String[] sqls) throws Exception{

        Class.forName(driver);
        try(
                Connection conn = DriverManager.getConnection(url, user, password)
                ){

            //關閉自動提交,開啓事務
            conn.setAutoCommit(false);
            try (
                    Statement stmt = conn.createStatement()
                    ){

                for (String sql : sqls){
                    stmt.executeUpdate(sql);
                }

            }

            //提交事務
            conn.commit();

        }


    }

    public static void main(String[] args) throws Exception{
        TransactionTest tt = new TransactionTest();
        tt.initParam("mysql.ini");

        String[] sqls = new String[]{
                "insert into student_table values(null, 'aaa', 1)",
                "insert into student_table values(null, 'bbb', 1)",
                "insert into student_table values(null, 'ccc', 1)",
                //下面這條SQL語句將會違反外鍵約束
                //因爲teacher_table表中沒有ID爲5的記錄
                "insert into student_table values(null, 'ccc', 5)"

        };

        tt.insertInTransaction(sqls);

    }


}

批量更新

爲了讓批量操作可以正確地處理錯誤,必須把批量執行的操作視爲單個事務,如果批量更新在執行過程中失敗,則讓事務回滾到批量操作開始之前的狀態。爲了達到這種效果,程序應該在開始批量操作之前先關閉自動提交,然後開始收集更新語句,當批量操作執行結束後,提交事務。

//保存當前的自動的提交模式
boolean autoCommit = conn.getAutoCommit();
//關閉自動提交
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
//使用Statement同時收集多條SQL語句
stmt.addBatch(sql1);
stmt.addBatch(sql2);
stmt.addBatch(sql2);
...
//提交修改
conn.commit();
//恢復原有的自動提交模式
conn.setAutoCommit(autoCommit);

使用系統表分析數據庫信息

MySQL數據庫使用information_schema數據庫來保存系統表,在該數據庫裏包含了大量系統表,常用系統表的簡單介紹如下。

  • tables:存放數據庫裏所有數據表的信息
  • schemata:存放數據庫裏所有數據庫的信息
  • views:存放數據庫裏所有視圖的信息
  • columns:存放數據庫裏所有列的信息
  • triggers:存放數據庫裏所有觸發器的信息
  • routines:存放數據庫裏所有存儲過程和函數的信息
  • key_column_usage:存放數據庫裏所有具有約束的鍵信息
  • table_constraints:存放數據庫裏全部約束的表信息
  • statistics:存放數據庫裏全部索引的信息

使用連接池管理連接

數據庫連接的建立及關閉是極耗費系統資源的操作,在多層結構的應用環境中,這種資源的耗費對系統性能影響尤爲明顯。通過DriverManager獲取數據庫連接,一個數據庫連接對象均對應一個物理數據庫連接,每次操作都打開一個物理連接,使用完後關閉連接。頻繁地打開、關閉連接將造成系統性能低下。

數據庫連接池的解決方案是:當應用程序啓動時,系統主動建立足夠的數據庫連接,並將這些連接組成一個連接池。每次應用程序請求數據庫連接時,無須重新打開連接,而是從連接池中取出已有的連接使用,使用完後不再關閉數據庫連接,而是直接將連接歸還給連接池。通過使用連接池,將大大提高程序的運行效率。

爲了解決數據庫連接的頻繁請求、釋放,JDBC 2.0規範引入了數據庫連接池技術。數據庫連接池是Connection對象的工廠。數據庫連接池的常用參數如下:

  • 數據庫的初始連接數
  • 連接池的最大連接數
  • 連接池的最小連接數
  • 連接池每次增加的容量

JDBC的數據庫連接池使用javax.sql.DataSource來表示,DataSource只是一個接口,該接口通常由商用服務器(如WebLogic、WebSphere)等提供實現,也有一些開源組織提供實現(如DBCP和C3P0等)。

DataSource通常被稱爲數據源,它包含連接池和連接池管理兩個部分,但習慣上我們也經常把DataSource稱爲連接池。

DBCP數據源

DBCP是Apache軟件基金組織下的開源連接池實現,該連接池依賴該組織下的另一個開源系統:common-pool。如果需要使用該連接池實現,則應在系統中增加如下兩個jar文件。

  • commons-dbcp.jar:連接池的實現
  • commons-pool.jar:連接池實現的依賴庫

Tomcat的連接池正是採用該連接池實現的。數據庫連接池既可以與應用服務器整合使用,也可以由應用程序獨立使用。下面的代碼片段示範了使用DBCP來獲得數據庫連接的方式

//創建數據源對象
BasicDataSource ds = new BasicDataSource();
//設置連接池所需的驅動
ds.setDriverClassName("com.mysql.jdbc.Driver");
//設置連接數據庫的URL
ds.setUrl("jdbc:mysql://localhost:3306/javaee");
//設置連接數據庫的用戶名
ds.setUsername("root");
//設置連接數據庫的密碼
ds.setPassword("pass");
//設置連接池的初始連接數
ds.setInitialSize(5);
//設置連接池最多可有多少個活動連接數
ds.setMaxActive(20);
//設置連接池中最少有2個空閒的連接
ds.setMinIdle(2);

數據源和數據庫連接不同,數據源無須創建多個,它是生產數據庫連接的工廠,因此整個應用只需要一個數據源即可。也就是說,對於一個應用,上面代碼只要執行一次即可。建議把上面程序中的ds設置成static成員變量,並且在應用開始時立即初始化數據源對象,程序中所有需要獲取數據庫連接的地方直接訪問該ds對象,並獲取數據庫連接即可。

//通過數據源獲取數據庫連接
Connection conn = ds.getConnection();

當數據庫訪問結束後,程序還是像以前一樣關閉數據庫連接。

//釋放數據庫連接
conn.close();

但上面代碼並沒有關閉數據庫的物理連接,它僅僅把數據庫連接釋放,歸還給連接池,讓其他客戶端可以使用該連接。

C3P0數據源

想比之下,C3P0數據源性能更勝一籌,Hibernate就推薦使用該連接池。C3P0連接池不僅可以自動清理不再使用的Connection,還可以自動清理Statement和ResultSet。如果需要使用C3P0連接池,則應在系統中增加如下jar文件。

  • c3p0-0.9.1.2.jar:C3P0連接池的實現

下面代碼通過C3P0連接池獲得數據庫連接

//創建連接池實例
ComboPooledDataSource ds = new ComboPooledDataSource();
//設置連接池連接數據庫所需的驅動
ds.setDriverClass("com.mysql.jdbc.Driver");
//設置連接數據庫的URL
ds.setJdbcUrl("jdbc:mysql://localhost:3306/javaee");
//設置連接數據庫的用戶名
ds.setUser("root");
//設置連接數據庫的密碼
ds.setPassword("32147");
//設置連接池的最大連接數
ds.setMaxPoolSize(40);
//設置連接池的最小連接數
ds.setMinPoolSize(2);
//設置連接池的初始連接數
ds.setInitialPoolSize(10);
//設置連接池的緩存Statement的最大數
ds.setMaxStatements(180);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章