【J2EE】JDBC

王曉斌發表於2014-04-18

參考: (JDBC常見面試題集錦1)http://blog.csdn.net/spidercoco/article/details/21439057

             (JDBC常見面試題集錦2)http://blog.csdn.net/spidercoco/article/details/21508763

JDBC簡介

JDBC介面及相關類在java.sql包和javax.sql包裡。我們可以用它來連線關係型資料庫,執行SQL查詢,儲存過程,並處理返回的結構。

JDBC介面讓Java程式和JDBC驅動實現了鬆耦合,使得切換不同的資料庫變得更加簡單。


JDBC API使用Java的反射機制來實現Java程式和JDBC驅動的鬆耦合。所有操作都是通過JDBC介面完成的,而驅動只有在通過Class.forName反射機制來載入的時候才會出現。

 

JDBC驅動程式型別


  • Type1:驅動程式將JDBC轉換成ODBC。使用ODBC驅動連線資料庫,需要安裝ODBC以便連線資料庫,這種方式已經基本淘汰了。
  • Type2:這種驅動把JDBC呼叫適配成資料庫的本地介面呼叫。
  • Type3:使用與具體資料庫無關的協議將資料庫請求傳送給伺服器中介軟體,然後該中介軟體再將資料庫請求翻譯成特定資料庫協議。這種方式增加了額外的網路呼叫,導致效能變差,因此很少使用。
  • Type4:這個驅動把JDBC請求直接轉化成特定資料庫使用的網路協議。這種方案最簡單,也適合通過網路連線資料庫。不過使用這種方式的話,需要根據不同的資料庫使用特定的驅動程式。

JDBC常用操作

建立JDBC連線 - Connection

JDBC連線時和資料庫伺服器建立的一個會話,可以想象成時一個和資料庫的Socket連線。

註冊並載入驅動

使用Class.forName(),驅動類就會註冊到DriverManager裡面並載入到記憶體中:

用DriverManager獲取連線物件

呼叫DriverManager.getConnection方法並傳入資料庫連線的URL,使用者名稱、密碼,就能獲取到連線物件。

建立JDBC連線所需的引數

建立JDBC聯結器需要提供四個基本資料:

  • JDBC驅動:driverClassName
  • 資料庫連線字:url
  • 資料庫訪問使用者名稱:username
  • 資料庫訪問密碼:password

JDBC驅動值

  • mysql:com.mysql.jdbc.Driver
  • oracle:oracle.jdbc.driver.OracleDriver
  • sql server:com.microsoft.jdbc.sqlserver.SQLServerDriver

資料庫連線字

  • mysql:jdbc:mysql://DBComputerNameOrIP:3306/DBName
  • oracle:jdbc:oracle:thin:@DBComputerNameOrIP:1512:DBName
  • sql server:jdbc:microsoft:sqlserver://DBComputerNameOrIP:1433;databaseName=DBName


獲取資料庫伺服器的相關資訊 - DatabaseMetaData

使用DatabaseMetaData可以獲取到伺服器的資訊。當和資料庫建立了連線之後,可以通過呼叫Connection的getMetaData方法獲取資料庫的元資訊。DatabaseMetaData裡面有很多方法,通過它們可以獲取到資料庫的產品名稱,版本號,配置資訊等。


執行資料庫SQL查詢語句 - Statement

Statement是JDBC中用來執行資料庫SQL查詢語句的介面。通過呼叫連線物件的createStatement方法可以生成一個Statement物件。通過呼叫它的execute,execeteQuery,executeUpdate方法來執行靜態SQL查詢。

預設情況下,一個Statement同時只能開啟一個ResultSet。如果想操作多個ResultSet的話,需要建立多個Statement。Statement介面的所有execute方法開始執行時都會預設關閉當前開啟的ResultSet。


execute

用來執行任意的SQL語句。如果查詢結果時一個ResultSet,這個方法就返回true。如果結果不是ResultSet,比如insert或update查詢,就會返回false。可以通過getResultSet方法來獲取ResultSet,或者通過getUpdateCount方法來回去更新的記錄條數。

Note:execute方法不能用於PreparedStatement或者CallableStatement。

executeQuery

用來執行select查詢,並且返回ResultSet。即使查詢不到記錄返回的ResultSet也不會為null(never null)。通常使用executeQuery來執行查詢語句,如果傳進來的是insert或者update的話,它會丟擲SQLException。

Note:execute方法不能用於PreparedStatement或者CallableStatement。

executeUpdate

用來執行inset,update或者delete語句(DML),或者什麼也不返回的DDL語句。如果時DML語句的話,返回更新的條數;如果時DDL語句的話,返回0.

Note:execute方法不能用於PreparedStatement或者CallableStatement。


只有當不確定時什麼語句的時候才應該使用execute方法,否則應該使用executeQuery或者executeUpdate方法。

getGeneratedKeys

有時候表會生成主鍵,這時候就可以用Statement的getGeneratedKeys方法來獲取這個自動生成的主鍵的值。

setMaxRows

可以用來限制返回的資料集的行數(通過SQL語句也可以實現這個功能)。

setFetchSize

當資料庫在執行一條查詢語句時,查詢到的資料是在資料庫的快取中維護的。ResultSet其實引用的是資料庫中快取的結果。

假設我們有一條查詢返回了100行資料,我麼把fetchsize設定成了10,那麼資料庫驅動每次只會取10條資料,也就是說得取10次。當每條資料需要處理的時間較長的時候並且返回資料又非常多的時候,這個可選引數就變的非常有用了。


執行預編譯的SQL語句 - PreparedStatement

public interface PreparedStatement extends Statement

代表的是一個預編譯的SQL語句,它提供的setter方法可以傳入查詢的變數。

由於PreparedStatement是預編譯的,通過它可以將對應的SQL語句高效的執行多次。由於PreparedStatement自動對特殊字元轉義,避免了SQL隱碼攻擊,因此應當儘量使用它。

PreparedStatement中注入null值

使用setNull方法把null值繫結到指定的變數上,setNull方法需要傳入引數的所以及SQL欄位的型別(i.e. java.sql.Types.VARCHAR)。

executeQuery無引數

執行SQL查詢語句,返回ResultSet(never null)。

executeUpdate無引數

執行insert,update,delete語句(DML),或者什麼也不返回的DDL語句。

如果時DML語句的話,返回更新的條數;如果時DDL語句的話,返回0.

PreparedStatement的優點

  • 有助於防止SQL隱碼攻擊,因此它會自動對特殊字元轉移;
  • 可以用來進行動態查詢;
  • 執行更快,尤其當重用它或者使用它的拼量查詢介面執行多條語句時;
  • 使用setter方法更容易寫出物件導向的程式碼;

PreparedStatement的缺點

不能直接用它來執行in條件語句。

解決辦法參見: http://www.journaldev.com/2521/jdbc-preparedstatement-in-clause-alternative-approaches#dynamic-prepared-statement


執行SQL儲存過程 - CallableStatement

通過JDBC的CallbleStatement介面來在資料庫中執行儲存過程。

參考:http://www.journaldev.com/2502/jdbc-callablestatement-stored-procedure-in-out-oracle-struct-cursor-example-tutorial


JDBC批處理

JDBC提供了批處理特性,可以在一次資料庫呼叫中執行多條查詢語句。

JDBC通過Statement和PreparedStatement中的addBatch和executeBatch方法來支援批處理。

參考: http://www.journaldev.com/2494/jdbc-batch-processing-example-tutorial-with-insert-statements


查詢結果集ResultSet

在查詢資料庫後會返回一個ResultSet,它就像查詢結果集的一張資料表。

ResultSet物件維護了一個遊標,指向當前的資料行。開始的時候這個遊標指向的是第一行。如果呼叫了ResultSet的next方法遊標會下移一行,如果沒有更多的資料了,next方法會返回false。可以在for迴圈中用它來遍歷資料集。

預設的ResultSet是不能更新的,遊標也只能往下移,也就是說只能從第一行到最後一行遍歷一遍。不過也可以建立可以回滾或者可更新的ResultSet,例如:

Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);

當生成ResultSet的Statement物件關閉或者重新執行或是獲取下一個ResultSet的時候,ResultSet物件也會自動關閉。

可以通過ResultSet的getter方法,傳入列名或者從1開始的序號來獲取列資料。

不同型別的ResultSet

根據建立 Statement時輸入引數的不同,會對應不同型別的ResultSet,以支援不同的ResultSet和併發型別。

  • ResultSet.TYPE_FORWARD_ONLY:預設的型別,遊標只能往下移;
  • ResultSet.TYPE_SCROLL_INSENSITIVE:遊標可以上下移動,一旦它建立後,資料庫裡的資料再發生修改,對它來說時透明的;
  • ResultSet.TYPE_SCROLL_SENSITIVE:遊標可以上下移動,如果生成後資料庫還發生了修改操作,它是能夠感知到的;

ResultSet有兩種併發型別:

  • ResultSet.CONCUR_READ_ONLY:ResultSet是隻讀的,這是預設型別;
  • ResultSet.CONCUR_UPDATABLE:我們可以使用ResultSet的更新方法來更新裡面的資料;

查詢結果集RowSet -  行集

public interface RowSet extends ResultSet  (javax.sql)

RowSet使用者儲存查詢的資料結果,和ResultSet相比,更靈活。

RowSet的特性:

  • 提供了Java Bean功能
  • RowSet物件預設時可滾動的,可更新的,因此,如果資料庫系統不支援ResultSet實現類似功能,可以使用RowSet使用

RowSet分為:

  • 連線型RowSet:這類物件與資料庫進行連線,和ResultSet類似;JDBC介面只提供了一種連線型RowSet, JdbcRowSet
  • 離線型RowSet:這類物件不需要和資料庫進行連線,因此它們更輕量級,更容易序列化。適用於在網路間傳遞資料。

四種不同型別的RowSet:

  • CachedRowSet:
  • WebRowSet:
  • JoinRowSet:
  • FilteredRowSet:

RowSet V.S. ResultSet

RowSet繼承自ResultSet.RowSet一個最大的好處是它可以是離線的,這樣使得它更輕量級,同時便於在網路間進行傳輸。

具體使用哪個取決於需求,不過如果操作ResultSet物件的事件較長的話,最好選擇一個離線的RowSet,這樣可以釋放資料庫連線。


JDBC事務管理

預設情況下,建立的資料庫連線(Connection),是在自動提交的模式下的。這意味著只要我們執行完一條查詢語句,就會自動進行提交。因此我們的每條查詢,實際上都是一個事物,如果我們執行的時DML或者DDL,每條語句完成的時候,資料庫就已經完成修改了。

有時候我們希望又一組SQL查詢組成一個事務,如果中途出現一場,可以進行回滾。

Connection的setAutoCommit方法,可以用來關閉自動提交的特性。應該在需要手動提交時才關閉這個特性。

資料庫通過表鎖來管理事務,這個操作非常消耗資源。因此應當完成操作後儘快的提交事務。

參考: http://www.journaldev.com/2483/jdbc-transaction-management-and-savepoint-example-tutorial


事務回滾

通過Connect的rollback方法可以回滾事務。它會回滾這次事務中的所有修改操作,並釋放當前連線所持有的資料庫鎖。


Savepoint儲存點

有時候事務儲存了一組語句,我們希望回滾到這個事務的某個特定的點。JDBC儲存點可以用來生成事務的一個檢查點,使得事務可以回滾到這個檢查點。

一旦事務提交或者回滾了,它生成的任何儲存點都會自動釋放並失效。回滾事務到某個特定的儲存點後,這個儲存點後所有其他的儲存點會自動釋放並且失效。

參考: http://www.journaldev.com/2483/jdbc-transaction-management-and-savepoint-example-tutorial


資料來源DataSource

是定義在javax.sql中的一個介面,跟DriverManager相比,它的功能要更強大。可以使用它來建立資料庫連線,驅動的實現類會實際去完成這個工作。

除了建立連線外,它還提供瞭如下的特性:

  • 快取PreparedStatement以便更快的執行;
  • 可以設定連線超時事件;
  • 提供日誌記錄的功能;
  • ResultSet大小的最大閾值設定;
  • 通過JNDI的支援,可以為servlet容器提供連線池的功能;

JDBC DataSource的示例參考: http://www.journaldev.com/2509/jdbc-datasource-example-oracle-mysql-and-apache-dbcp-tutorial


在Servlet容器中設定連線池

參考:http://www.journaldev.com/2513/tomcat-datasource-jndi-example-for-servlet-web-application


Apache的DBCP

如果用DataSource來獲取連線的話,通常獲取連線的程式碼和驅動特定的DataSource是緊耦合的。另外,除了選擇DataSource的實現類,剩下的程式碼基本都是一樣的。

Apache的DBCP(commons-pool)用來解決這些問題,它提供的DataSource實現成為了應用程式和不同JDBC驅動間的一個抽象層。

參考:http://www.journaldev.com/2509/jdbc-datasource-example-oracle-mysql-and-apache-dbcp-tutorial


其他

JDBC資料庫隔離級別

事物隔離級別的前提是一個多使用者,多執行緒,多程式的併發系統,在這個系統中為了保證資料的一致性和完整性,引入了事務隔離級別的概念。對一個單使用者,單執行緒的應用不存在這個問題。


高併發系統中存在的問題

髒讀

一個事務讀到另一個事務還沒有提交的資料,稱之為髒讀。

當我們使用事務時,有可能出現這樣的情況,有一行資料剛更新,與此同時另一個查詢讀到了這個剛更新的值,這樣就導致了髒讀,因為更新的資料還沒有進行持久化,更新這行資料的業務可能會進行回滾,這樣這個資料就是無效的。

解決辦法:如果在第一個事務提交前,任何其他事物不可讀取其修改過的值,則可以避免該問題。

不可重複讀

一個事務先後讀取同一條記錄,但兩次讀取的資料不同,我們稱之為不可重複讀。

解決辦法:如果只有在修改事務完全提交之後才可以讀取資料,則可以避免該問題。

幻讀

一個事務先後讀取一個範圍的記錄,但兩次讀取的記錄數不同,稱之為幻讀。

解決辦法:如果在操作事務完成資料處理之前,任何其他事務都不可以新增新資料,則可以避免該問題。

不可重複讀 V.S. 幻讀

不可重複讀的重點是修改:同樣的提交,讀取過的資料,再次讀取出來發現值不一樣了。

幻讀的重點在於新增或者刪除:同樣的條件,第一次和第二次讀出來的記錄數不一樣。

從控制的角度看:

不可重複讀:只需要鎖住滿足條件的記錄。

幻讀:要鎖住滿足條件及其相近的記錄。


參考:http://jianfulove.iteye.com/blog/1843963

JDBC隔離級別

當使用事務時,資料庫系統用鎖來防止別人訪問事務中用到的資料。資料庫通過鎖來防止髒讀,不可重複讀及幻讀的問題。

資料庫使用JDBC設定的隔離級別來決定它使用何種鎖機制。我們可以通過Connection的getTransactionIsolation和setTransactionIsolation方法來獲取和設定資料庫的隔離級別。

隔離級別 事務 髒讀 不可重複讀 幻讀
TRANSACTION_NONE 不支援 不可用 不可用 不可用
TRANSACTION_READ_COMMITTED 支援 阻止 允許 允許
TRANSACTION_READ_UNCOMMITTED 支援 允許 允許 允許
TRANSACTION_REPEATABLE_READ 支援 阻止 阻止 允許
TRANSACTION_SERIALIZABLE 支援 阻止 阻止 阻止

java.sql.Connection:

Fields 
Modifier and Type Field and Description
static int TRANSACTION_NONE
A constant indicating that transactions are not supported.
static int TRANSACTION_READ_COMMITTED
A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur.
static int TRANSACTION_READ_UNCOMMITTED
A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur.
static int TRANSACTION_REPEATABLE_READ
A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur.
static int TRANSACTION_SERIALIZABLE
A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented.

JDBC異常

  • java.sql.SQLException - JDBC異常的基類;
  • java.sql.BatchUpdateException - 當批處理操作執行失敗的時候可能會丟擲這個一場,這取決於具體的JDBC驅動的實現,它也可能直接丟擲異常基類SQLException;
  • java.sql.SQLWarning - SQL操作出現的警告資訊;
  • java.sql.DataTrunction - 欄位值由於某些非正常原因被截斷了 (不是因為超過對應欄位型別的長度限制而被截斷的);

SQLWarning

SQLWarning是SQLException的自雷,通過Connection,Statement,ResultSet的getWarnings方法都可以獲取到它。SQLWarning不會中斷查詢語句的執行,只是用來提示使用者存在相關的警告資訊。


JDBC中的其他資料型別

CLOB

public interface Clob (java.sql)

Character Large Object字元大物件;它是由單位元組字元組成的字串資料,有自己專門的內碼表。這種資料型別適用於儲存超長的文字資訊,那麼可能會超出標準的VARCHAR型別長度限制的文字。

BLOB

public interface Blob  (java.sql)

Binary Large Object二進位制大物件;由二進位制資料組成,沒有專門的內碼表。它能用於儲存超過VARBINARY限制的二進位制資料。這種型別適合儲存圖片,聲音,圖形,或者其他業務程式特定的資料。

Date

public class Timestamp extends Date  (java.sql)

Date只儲存日期資料不儲存時間資料。

Timestamp


總結

  • 資料庫資源是非常昂貴的,用完了應該儘快關閉它。Connection,Statement,ResultSet等JDBC物件都有close方法;
  • 養成在程式碼中顯式關閉掉ResultSet,Statement,Connection的習慣;如果使用的是連線池的話,連線用完後會放回池裡,但是沒有關閉的ResultSet和Statement就會造成資源洩漏;
  • 在finally塊中關閉資源,保證即便出現了異常也能正常關閉;
  • 大量類似的查詢應當使用批處理完成;
  • 儘量使用PreparedStatement而不是Statement,以避免SQL隱碼攻擊,同時還能通過預編譯和快取機制提升執行的效率;
  • 如果要講大量資料讀入到ResultSet中,應該合理的設定fetchSize以便提升效能;
  • 檢查使用資料所支援的隔離級別;
  • 資料庫隔離級別越高效能越差,確保使用的資料庫連線設定級別是最優的;
  • 如果需要長時間對ResultSet進行操作的話,儘量使用離線的RowSet;