1.闡述JDBC操作資料庫的步驟。
答:下面的代碼以串連原生Oracle資料庫為例,示範JDBC操作資料庫的步驟。 載入驅動。
Class.forName("oracle.jdbc.driver.OracleDriver"); 建立串連。
Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "scott", "tiger"); 建立語句。
PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?"); ps.setInt(1, 1000); ps.setInt(2, 3000); 執行語句
ResultSet rs = ps.executeQuery();
處理結果。
while(rs.next()) { System.out.println(rs.getInt("empno") + " - " + rs.getString("ename")); } 關閉資源。
finally { if(con != null) { try { con.close(); } catch (SQLException e) { e.printStackTrace(); } } }
提示:關閉外部資源的順序應該和開啟的順序相反,也就是說先關閉ResultSet、再關閉Statement、在關閉Connection。上面的代碼只關閉了Connection(串連),雖然通常情況下在關閉串連時,串連上建立的語句和開啟的遊標也會關閉,但不能保證總是如此,因此應該按照剛才說的順序分別關閉。此外,第一步載入驅動在JDBC 4.0中是可以省略的(自動從類路徑中載入驅動),但是我們建議保留。
2.Statement和PreparedStatement有什麼區別。哪個效能更好。
答:與Statement相比,①PreparedStatement介面代表先行編譯的語句,它主要的優勢在於可以減少SQL的編譯錯誤並增加SQL的安全性(減少SQL注射攻擊的可能性);②PreparedStatement中的SQL語句是可以帶參數的,避免了用字串串連拼接SQL語句的麻煩和不安全;③當批量處理SQL或頻繁執行相同的查詢時,PreparedStatement有明顯的效能上的優勢,由於資料庫可以將編譯最佳化後的SQL語句緩衝起來,下次執行相同結構的語句時就會很快(不用再次編譯和產生執行計畫)。
補充:為了提供對預存程序的調用,JDBC API中還提供了CallableStatement介面。預存程序(Stored Procedure)是資料庫中一組為了完成特定功能的SQL語句的集合,經編譯後儲存在資料庫中,使用者通過指定預存程序的名字並給出參數(如果該預存程序帶有參數)來執行它。雖然調用預存程序會在網路開銷、安全性、效能上獲得很多好處,但是存在如果底層資料庫發生遷移時就會有很多麻煩,因為每種資料庫的預存程序在書寫上存在不少的差別。
3.使用JDBC操作資料庫時,如何提升讀取資料的效能。如何提升更新資料的效能。
答:要提升讀取資料的效能,可以指定通過結果集(ResultSet)對象的setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提升更新資料的效能可以使用PreparedStatement語句構建批處理,將若干SQL語句置於一個批處理中執行。
4.在進行資料庫編程時,串連池有什麼作用。
答:由於建立串連和釋放串連都有很大的開銷(尤其是資料庫伺服器不在本地時,每次建立串連都需要進行TCP的三向交握,釋放串連需要進行TCP四次握手,造成的開銷是不可忽視的),為了提升系統訪問資料庫的效能,可以事先建立若干串連置於串連池中,需要時直接從串連池擷取,使用結束時歸還串連池而不必關閉串連,從而避免頻繁建立和釋放串連所造成的開銷,這是典型的用空間換取時間的策略(浪費了空間儲存串連,但節省了建立和釋放串連的時間)。池化技術在Java開發中是很常見的,在使用線程時建立線程池的道理與此相同。基於Java的開來源資料庫串連池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。
補充:在電腦系統中時間和空間是不可調和的矛盾,理解這一點對設計滿足效能要求的演算法是至關重要的。大型網站效能最佳化的一個關鍵就是使用緩衝,而緩衝跟上面講的串連池道理非常類似,也是使用空間換時間的策略。可以將熱點資料置於緩衝中,當使用者查詢這些資料時可以直接從緩衝中得到,這無論如何也快過去資料庫中查詢。當然,緩衝的置換策略等也會對系統效能產生重要影響,對於這個問題的討論已經超出了這裡要闡述的範圍。
5.什麼是DAO模式。
答:DAO(Data Access Object)顧名思義是一個為資料庫或其他持久化機制提供了抽象介面的對象,在不暴露底層持久化方案實現細節的前提下提供了各種資料訪問操作。在實際的開發中,應該將所有對資料來源的訪問操作進行抽象化後封裝在一個公用API中。用程式設計語言來說,就是建立一個介面,介面中定義了此應用程式中將會用到的所有事務方法。在這個應用程式中,當需要和資料來源進行互動的時候則使用這個介面,並且編寫一個單獨的類來實現這個介面,在邏輯上該類對應一個特定的資料存放區。DAO模式實際上包含了兩個模式,一是Data Accessor(資料訪問器),二是Data Object(資料對象),前者要解決如何訪問資料的問題,而後者要解決的是如何用對象封裝資料。
6.事務的ACID是指什麼。
答:
- 原子性(Atomic):事務中各項操作,要麼全做要麼全不做,任何一項操作的失敗都會導致整個事務的失敗;
- 一致性(Consistent):事務結束後系統狀態是一致的;
- 隔離性(Isolated):並發執行的事務彼此無法看到對方的中間狀態;
- 持久性(Durable):事務完成後所做的改動都會被持久化,即使發生災難性的失敗。通過日誌和同步備份可以在故障發生後重建資料。
補充:關於事務,在面試中被問到的機率是很高的,可以問的問題也是很多的。首先需要知道的是,只有存在並發資料訪問時才需要事務。當多個事務訪問同一資料時,可能會存在5類問題,包括3類資料讀取問題(髒讀、不可重複讀取和幻讀)和2類資料更新問題(第1類丟失更新和第2類丟失更新)。
髒讀(Dirty Read):A事務讀取B事務尚未提交的資料並在此基礎上操作,而B事務執行復原,那麼A讀取到的資料就是髒資料。
| 時間 |
轉賬事務A |
取款事務B |
| T1 |
|
開始事務 |
| T2 |
開始事務 |
|
| T3 |
|
查詢賬戶餘額為1000元 |
| T4 |
|
取出500元餘額修改為500元 |
| T5 |
查詢賬戶餘額為500元(髒讀) |
|
| T6 |
|
撤銷事務餘額恢複為1000元 |
| T7 |
匯入100元把餘額修改為600元 |
|
| T8 |
提交事務 |
|
不可重複讀取(Unrepeatable Read):事務A重新讀取前面讀取過的資料,發現該資料已經被另一個已提交的事務B修改過了。
| 時間 |
轉賬事務A |
取款事務B |
| T1 |
|
開始事務 |
| T2 |
開始事務 |
|
| T3 |
|
查詢賬戶餘額為1000元 |
| T4 |
查詢賬戶餘額為1000元 |
|
| T5 |
|
取出100元修改餘額為900元 |
| T6 |
|
提交事務 |
| T7 |
查詢賬戶餘額為900元(不可重複讀取) |
|
幻讀(Phantom Read):事務A重新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務B提交的行。
| 時間 |
統計金額事務A |
轉賬事務B |
| T1 |
|
開始事務 |
| T2 |
開始事務 |
|
| T3 |
統計總存款為10000元 |
|
| T4 |
|
新增一個存款賬戶存入100元 |
| T5 |
|
提交事務 |
| T6 |
再次統計總存款為10100元(幻讀) |
|
第1類丟失更新:事務A撤銷時,把已經提交的事務B的更新資料覆蓋了。
| 時間 |
取款事務A |
轉賬事務B |
| T1 |
開始事務 |
|
| T2 |
|
開始事務 |
| T3 |
查詢賬戶餘額為1000元 |
|
| T4 |
|
查詢賬戶餘額為1000元 |
| T5 |
|
匯入100元修改餘額為1100元 |
| T6 |
|
提交事務 |
| T7 |
取出100元將餘額修改為900元 |
|
| T8 |
撤銷事務 |
|
| T9 |
餘額恢複為1000元(丟失更新) |
|
第2類丟失更新:事務A覆蓋事務B已經提交的資料,造成事務B所做的操作丟失。
| 時間 |
轉賬事務A |
取款事務B |
| T1 |
|
開始事務 |
| T2 |
開始事務 |
|
| T3 |
|
查詢賬戶餘額為1000元 |
| T4 |
查詢賬戶餘額為1000元 |
|
| T5 |
|
取出100元將餘額修改為900元 |
| T6 |
|
提交事務 |
| T7 |
匯入100元將餘額修改為1100元 |
|
| T8 |
提交事務 |
|
| T9 |
查詢賬戶餘額為1100元(丟失更新) |
|
資料並發訪問所產生的問題,在有些情境下可能是允許的,但是有些情境下可能就是致命的,資料庫通常會通過鎖機制來解決資料並發訪問問題,按鎖定對象不同可以分為表級鎖和行級鎖;按並發事務鎖定關係可以分為共用鎖定和獨佔鎖,具體的內容大家可以自行查閱資料進行瞭解。
直接使用鎖是非常麻煩的,為此資料庫為使用者提供了自動鎖機制,只要使用者指定會話的交易隔離等級,資料庫就會通過分析SQL語句然後為事務訪問的資源加上合適的鎖,此外,資料庫還會維護這些鎖通過各種手段提高系統的效能,這些對使用者來說都是透明的(就是說你不用理解,事實上我確實也不知道)。ANSI/ISO SQL 92標準定義了4個等級的交易隔離等級,如下表所示:
| 隔離等級 |
髒讀 |
不可重複讀取 |
幻讀 |
第一類丟失更新 |
第二類丟失更新 |
| READ UNCOMMITED |
允許 |
允許 |
允許 |
不允許 |
允許 |
| READ COMMITTED |
不允許 |
允許 |
允許 |
不允許 |
允許 |
| REPEATABLE READ |
不允許 |
不允許 |
允許 |
不允許 |
不允許 |
| SERIALIZABLE |
不允許 |
不允許 |
不允許 |
不允許 |
不允許 |
需要說明的是,交易隔離等級和資料訪問的並發性是對立的,交易隔離等級越高並發性就越差。所以要根據具體的應用來確定合適的交易隔離等級,這個地方沒有萬能的原則。
7.JDBC中如何進行交易處理。
答:Connection提供了交易處理的方法,通過調用setAutoCommit(false)可以設定手動提交事務;當事務完成後用commit()顯式提交事務;如果在交易處理過程中發生異常則通過rollback()進行交易回復。除此之外,從JDBC 3.0中還引入了Savepoint(儲存點)的概念,允許通過代碼設定儲存點並讓交易回復到指定的儲存點。
8.JDBC能否處理Blob和Clob。
答: Blob是指二進位大對象(Binary Large Object),而Clob是指大字元對象(Character Large Objec),因此其中Blob是為儲存大的位元據而設計的,而Clob是為儲存大的文本資料而設計的。JDBC的PreparedStatement和ResultSet都提供了相應的方法來支援Blob和Clob操作。下面的代碼展示了如何使用JDBC操作LOB:
下面以MySQL資料庫為例,建立一個張有三個欄位的使用者表,包括編號(id)、姓名(name)和照片(photo),建表語句如下:
create table tb_user(id int primary key auto_increment,name varchar(20) unique not null,photo longblob);
下面的Java代碼向資料庫中插入一條記錄:
import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.sql.SQLException;class JdbcLobTest { public static void main(String[] args) { Connection con = null; try { // 1. 載入驅動(Java6以上版本可以省略) Class.forName("com.mysql.jdbc.Driver"); // 2. 建立串連 con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456"); // 3. 建立語句對象 PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)"); ps.setString(1, "張三"); // 將SQL語句中第一個預留位置換成字串 try (InputStream in = new FileInputStream("test.jpg")) { // Java 7的TWR ps.setBinaryStream(2, in); // 將SQL語句中第二個預留位置換成二進位流 // 4. 發出SQL語句獲得受影響行數 System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失敗"); } catch(IOException e) { System.out.println("讀取照片失敗!"); } } catch (ClassNotFoundException | SQLException e) { // Java 7的多異常捕獲 e.printStackTrace(); } finally { // 釋放外部資源的代碼都應當放在finally中保證其能夠得到執行 try { if(con != null && !con.isClosed()) { con.close(); // 5. 釋放資料庫連接 con = null; // 指示記憶體回收行程可以回收該對象 } } catch (SQLException e) { e.printStackTrace(); } } }}