Java回顧之JDBC
概述
盡管在實際開發過程中,我們一般使用ORM框架來代替傳統的JDBC,例如Hibernate或者iBatis,但JDBC是Java用來實現數據訪問的基礎,掌握它對于我們理解Java的數據操作流程很有幫助。
JDBC的全稱是Java Database Connectivity。
JDBC對數據庫進行操作的流程:
- 連接數據庫
- 發送數據請求,即傳統的CRUD指令
- 返回操作結果集
JDBC中常用的對象包括:
- ConnectionManager
- Connection
- Statement
- CallableStatement
- PreparedStatement
- ResultSet
- SavePoint
一個簡單示例
我們來看下面一個簡單的示例,它使用JDK自帶的Derby數據庫,創建一張表,插入一些記錄,然后將記錄返回:
- private static void test1() throws SQLException
- {
- String driver = "org.apache.derby.jdbc.EmbeddedDriver";
- String dbURL = "jdbc:derby:EmbeddedDB;create=true";
- Connection con = null;
- Statement st = null;
- try
- {
- Class.forName(driver);
- con = DriverManager.getConnection(dbURL);
- st = con.createStatement();
- st.execute("create table foo(ID INT NOT NULL, NAME VARCHAR(30))");
- st.executeUpdate("insert into foo(ID,NAME) values(1, 'Zhang San')");
- ResultSet rs = st.executeQuery("select ID,NAME from foo");
- while(rs.next())
- {
- int id = rs.getInt("ID");
- String name = rs.getString("NAME");
- System.out.println("ID=" + id + "; NAME=" + name);
- }
- }
- catch(Exception ex)
- {
- ex.printStackTrace();
- }
- finally
- {
- if (st != null) st.close();
- if (con != null) con.close();
- }
- }
如何建立數據庫連接
上面的示例代碼中,建立數據庫連接的部分如下:
String driver = "org.apache.derby.jdbc.EmbeddedDriver"; String dbURL = "jdbc:derby:EmbeddedDB;create=true"; Class.forName(driver); con = DriverManager.getConnection(dbURL);
建立數據庫連接的過程,可以分為兩步:
1)加載數據庫驅動,即上文中的driver以及Class.forName(dirver)
2)定位數據庫連接字符串, 即dbURL以及DriverManager.getConnection(dbURL)
不同的數據庫,對應的dirver和dbURL不同,但加載驅動和建立連接的方式是相同的,即只需要修改上面driver和dbURL的值就可以了。
自動加載數據庫驅動
如果我們每次建立連接時,都要使用Class.forName(...)來手動加載數據庫驅動,這樣會很麻煩,我們可以通過配置文件的方式,來保存數據庫驅動的信息。
我們可以在classpath中,即編譯出來的.class的存放路徑,添加如下文件:
META-INF\services\java.sql.Driver
對應的內容就是JDBC驅動的全路徑,也就是上面driver變量的值:
org.apache.derby.jdbc.EmbeddedDriver
接下來,我們在程序中,就不需要再顯示的用Class.forName(...)來加載驅動了,它會被自動加載進來,當我們的數據庫發生變化時,只需要修改這個文件就可以了,例如當我們的數據庫由Derby變為MySQL時,只需要將上述的配置修改為:
com.mysql.jdbc.Driver
但是,需要注意一點,這里只是配置了JDBC驅動的全路徑,并沒有包含jar文件的信息,因此,我們還是需要將包含該驅動的jar文件手動的放置到程序的classpath中。
JDBC中的基本操作
對于數據庫操作來說,CRUD操作應該是最常見的操作了, 即我們常說的增、刪、查、改。
JDBC是使用Statement和ResultSet來完成這些操作的。
如何實現CRUD
下面是一個實現CRUD的示例:
我們順序調用上面的測試方法:
1 insertTest(); 2 deleteTest(); 3 updateTest();
執行結果如下:
=====insert test===== ID:1; NAME=Zhang San ID:2; NAME=Li Si ID:3; NAME=Wang Wu =====delete test===== ID:1; NAME=Zhang San ID:2; NAME=Li Si =====update test===== ID:1; NAME=Zhang San ID:2; NAME=TEST
上面代碼中的showUser方法會把user表中的所有記錄打印出來。
#p#
如何調用存儲過程
存儲過程是做數據庫開發時經常使用的技術,它可以通過節省編譯時間的方式來提升系統性能,我們這里的示例使用MySQL數據庫。
如何調用不帶參數的存儲過程
假設我們現在有一個簡單的存儲過程,它只是返回user表中的所有記錄,存儲過程如下:
- CREATE DEFINER=`root`@`localhost` PROCEDURE `GetUser`()
- BEGIN
- select ID,NAME from user;
- END
我們可以使用CallableStatement來調用存儲過程:
它的執行結果如下:
ID:1; NAME=Zhang San ID:2; NAME=TEST
如何調用帶參數的存儲過程
MySQL的存儲過程中的參數分為三種:in/out/inout,我們可以把in看做入力參數,out看做出力參數,JDBC對這兩種類型的參數設置方式不同:
1)in, JDBC使用類似于cst.set(1, 10)的方式來設置
2)out,JDBC使用類似于cst.registerOutParameter(2, Types.VARCHAR);的方式來設置
我們來看一個in參數的示例,假設我們希望返回ID為特定值的user信息,存儲過程如下:
- CREATE DEFINER=`root`@`localhost` PROCEDURE `GetUserByID`(in id int)
- BEGIN
- set @sqlstr=concat('select * from user where ID=', id);
- prepare psmt from @sqlstr;
- execute psmt;
- END
Java的調用代碼如下:
我們執行下面的語句:
execStoredProcedureTest2(1);
結果如下:
ID:1; NAME=Zhang San
對于out類型的參數,調用方式類似,不再贅述。
獲取數據庫以及結果集的metadata信息
在JDBC中,我們不僅能夠對數據進行操作,我們還能獲取數據庫以及結果集的元數據信息,例如數據庫的名稱、驅動信息、表信息;結果集的列信息等。
獲取數據庫的metadata信息
我們可以通過connection.getMetaData方法來獲取數據庫的元數據信息,它的類型是DatabaseMetaData。
這里我們使用的數據庫是MySQL中自帶的默認數據庫:mysql,它會記錄整個數據庫服務器中的一些信息。上述代碼執行結果如下:
數據庫:MySQL 5.5.28 驅動程序:MySQL-AB JDBC Driver mysql-connector-java-5.0.4 ( $Date: 2006-10-19 17:47:48 +0200 (Thu, 19 Oct 2006) $, $Revision: 5908 $ ) |表名稱 |表類別 |表類型 |表模式 | |columns_priv |mysql |TABLE |null | |db |mysql |TABLE |null | |event |mysql |TABLE |null | |func |mysql |TABLE |null | 。。。
由于mysql中表比較多,上述結果只截取了一部分。
獲取結果集的元數據信息
我們可以通過使用resultset.getMetaData方法來獲取結果集的元數據信息,它的類型是ResultSetMetaData。
它的執行結果如下:
Column Name:ID; Column Type:INTEGER UNSIGNED Column Name:NAME; Column Type:VARCHAR
可以看到,它返回類結果集中每一列的名稱和類型。
基于ResultSet的操作
當我們需要對數據庫進行修改時,除了上述通過Statement完成操作外,我們也可以借助ResultSet來完成。
需要注意的是,在這種情況下,我們定義Statement時,需要添加參數。
Statement構造函數可以包含3個參數:
resultSetType
,它的取值包括:ResultSet.TYPE_FORWARD_ONLY
、ResultSet.TYPE_SCROLL_INSENSITIVE
或ResultSet.TYPE_SCROLL_SENSITIVE,默認情況下,該參數的值是
ResultSet.TYPE_FORWARD_ONLY
。resultSetConcurrency
,它的取值包括:ResultSet.CONCUR_READ_ONLY
或ResultSet.CONCUR_UPDATABLE
,默認情況下,該參數的值是ResultSet.CONCUR_READ_ONLY
。resultSetHoldability
,它的取值包括:ResultSet.HOLD_CURSORS_OVER_COMMIT
或ResultSet.CLOSE_CURSORS_AT_COMMIT
。
為了使得ResultSet能夠對數據進行操作我們需要:
- 將resultSetType設置為
ResultSet.TYPE_SCROLL_SENSITIVE
。 - 將resultSetConcurrency設置為
ResultSet.CONCUR_UPDATABLE
。
在通過ResultSet對數據進行調整的過程中,下面方法可能會被調用:
- resultset.last()
- resultset.first()
- resultset.moveToInsertRow()
- resultset.absolute()
- resultset.setxxx()
- resultset.updateRow()
- resultset.insertRow()
下面是一個通過ResultSet對數據進行增、刪、改的示例:
分別調用上述方法:
1 getResultCount(); 2 insertDataToResultSet(); 3 updateDataToResultSet(); 4 delDataFromResultSet();
執行結果如下:
=====Result Count===== 返回結果的條數:2 =====Insert===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:4; NAME=Xiao Ming =====Update===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:4; NAME=Xiao Qiang =====Delete===== ID:1; NAME=Zhang San ID:2; NAME=TEST
可以看到我們對ID為4的記錄進行了插入、更新和刪除操作。
#p#
預處理以及批處理
預處理和批處理都是用來提升系統性能的方式,一種是利用數據庫的緩存機制,一種是利用數據庫一次執行多條語句的方式。
預處理
數據庫服務器接收到Statement后,一般會解析Statement、分析是否有語法錯誤、定制最優的執行計劃,這個過程可能會降低系統的性能。一般的數據庫服務器都這對這種情況,設計了緩存機制,當數據庫接收到指令時,如果緩存中已經存在,那么就不再解析,而是直接運行。
這里相同的指令是指sql語句完全一樣,包括大小寫。
JDBC使用PreparedStatement來完成預處理:
執行結果如下:
=====Insert a single record by PreparedStatement===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng
批處理
批處理是利用數據庫一次執行多條語句的機制來提升性能,這樣可以避免多次建立連接帶來的性能損失。
批處理使用Statement的addBatch來添加指令,使用executeBatch方法來一次執行多條指令:
執行結果如下:
=====Insert multiple records by Statement & Batch===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:6; NAME=Xiao Zhang ID:7; NAME=Xiao Liu ID:8; NAME=Xiao Zhao
預處理和批處理相結合
我們可以把預處理和批處理結合起來,利用數據庫的緩存機制,一次執行多條語句:
執行結果如下:
=====Insert multiple records by PreparedStatement & Batch===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:9; NAME=Xiao Zhang ID:10; NAME=Xiao Liu ID:11; NAME=Xiao Zhao
數據庫事務
談到數據庫開發,事務是一個不可回避的話題,JDBC默認情況下,是每一步都自動提交的,我們可以通過設置 connection.setAutoCommit(false)的方式來強制關閉自動提交,然后通過connection.commit()和 connection.rollback()來實現事務提交和回滾。
簡單的數據庫事務
下面是一個簡單的數據庫事務的示例:
連續執行上述方法兩次,我們可以得出下面的結果:
=====Simple Transaction test===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:12; NAME=Xiao Li =====Simple Transaction test===== ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:12; NAME=Xiao Li com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '12' for key 'PRIMARY' at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:931) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2870) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1573) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1665) at com.mysql.jdbc.Connection.execSQL(Connection.java:3170) at com.mysql.jdbc.Statement.executeUpdate(Statement.java:1316) at com.mysql.jdbc.Statement.executeUpdate(Statement.java:1235) at sample.jdbc.mysql.ResultSetSample.transactionTest1(ResultSetSample.java:154) at sample.jdbc.mysql.ResultSetSample.main(ResultSetSample.java:17)
可以看到,第一次調用時,操作成功,事務提交,向user表中插入了一條記錄;第二次調用時,發生主鍵沖突異常,事務回滾。
帶有SavePoint的事務
當我們的事務操作中包含多個處理,但我們有時希望一些操作完成后可以先提交,這樣可以避免整個事務的回滾。JDBC使用SavePoint來實現這一點。
執行結果如下:
=====Simple Transaction test===== com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry '13' for key 'PRIMARY' at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:931) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2870) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1573) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1665) at com.mysql.jdbc.Connection.execSQL(Connection.java:3170) at com.mysql.jdbc.Statement.executeUpdate(Statement.java:1316) at com.mysql.jdbc.Statement.executeUpdate(Statement.java:1235) at sample.jdbc.mysql.ResultSetSample.transactionTest2(ResultSetSample.java:185) at sample.jdbc.mysql.ResultSetSample.main(ResultSetSample.java:18) ID:1; NAME=Zhang San ID:2; NAME=TEST ID:5; NAME=Lei Feng ID:13; NAME=Xiao Li ID:14; NAME=Xiao Wang
可以看到最終事務報出了主鍵沖突異常,事務回滾,但是依然向數據庫中插入了ID為13和14的記錄。
另外,在確定SavePoint后,ID為15的記錄并沒有被插入,它是通過事務進行了回滾。
原文鏈接:http://www.cnblogs.com/wing011203/archive/2013/05/12/3073838.html