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的處理有如下兩種常見方式。
- 使用迭代訪問ResultSet裏的記錄,並將這些記錄轉換成Java Bean,再將多個Java Bean封裝成一個List集合,也就是完成
ResultSet——>Java Bean集合
的轉換。轉換完成後可以關閉Connection等資源,然後將Java Bean集合傳到視圖顯示層,視圖顯示層可以顯示查詢得到的數據。 - 直接將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個特性
- 原子性(Atomicity):事務是應用中最小的執行單位,就如原子是自然界的最小顆粒,具有不可再分的特徵一樣,事務是應用中不可再分的最小邏輯執行體。
- 一致性(Consistency):事務執行的結果,必須使數據庫從一個一致性狀態,變到另一個一致性狀態。
- 隔離性(Isolation):各個事務的執行互不干擾,任意一個事務的內部操作對其他併發的事務都是隔離的,也就是說,併發執行的事務之間不能看到對方的中間狀態,併發執行的事務之間不能互相影響。
- 持續性(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);