更多內(nèi)容,歡迎關(guān)注微信公眾號:全菜工程師小輝~
前幾天筆者發(fā)布了博客,手寫mybatis徹底搞懂框架原理。為了幫助初學(xué)者更好理解mybatis框架,這次講解一下JAVA的JDBC的運(yùn)行過程。
JDBC的作用
JDBC的全稱是Java DataBase Connection,也就是Java數(shù)據(jù)庫連接,我們可以用它來操作關(guān)系型數(shù)據(jù)庫。JDBC接口及相關(guān)類在java.sql包和javax.sql包里。我們可以用它來連接數(shù)據(jù)庫,執(zhí)行SQL查詢,存儲過程,并處理返回的結(jié)果。
JDBC接口讓Java程序和JDBC驅(qū)動實(shí)現(xiàn)了松耦合,使得切換不同的數(shù)據(jù)庫變得更加簡單。

JDBC
JDBC的連接步驟
執(zhí)行一次JDBC連接,分六個步驟進(jìn)行:
1. 導(dǎo)入包
在程序中包含數(shù)據(jù)庫編程所需的JDBC類。大多數(shù)情況下,使用 import java.sql.* 就足夠了
2. 注冊JDBC驅(qū)動程序
需要初始化驅(qū)動程序,這樣就可以打開與數(shù)據(jù)庫的通信。
3. 打開一個連接
使用DriverManager.getConnection()方法來創(chuàng)建一個Connection對象,它代表一個數(shù)據(jù)庫的物理連接。
4. 執(zhí)行一個查詢
需要使用一個類型為Statement或PreparedStatement的對象(兩者區(qū)別看后文),并提交一個SQL語句到數(shù)據(jù)庫執(zhí)行查詢。
5. 從結(jié)果集中提取數(shù)據(jù)
這一步中演示如何從數(shù)據(jù)庫中獲取查詢結(jié)果的數(shù)據(jù)。使用ResultSet.getXXX()方法來檢索的數(shù)據(jù)結(jié)果
6. 清理環(huán)境資源
在使用JDBC與數(shù)據(jù)交互操作數(shù)據(jù)庫中的數(shù)據(jù)后,應(yīng)該明確地關(guān)閉所有的數(shù)據(jù)庫資源以減少資源的浪費(fèi)。本文使用了try with resources方式關(guān)閉資源,這是JDK7的語法糖,讀者可自行搜索。
完整代碼如下。
//STEP 1. 導(dǎo)入包 import java.sql.*; class JDBCExample { // JDBC驅(qū)動包名和數(shù)據(jù)庫的URL static final String JDBC_DRIVER = "com.MySQL.jdbc.Driver"; static final String DB_URL = "jdbc:mysql://localhost/test"; // 數(shù)據(jù)庫名和密碼自己修改 static final String USER = "username"; static final String PASS = "password"; public static void main(String[] args) { String sql = "SELECT id, first, last, age FROM Employees"; //STEP 2: 注冊JDBC驅(qū)動程序 try { Class.forName(JDBC_DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } // try with resources方式關(guān)閉資源。 //STEP 6: 清理環(huán)境資源 try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { //STEP 3: 打開一個連接 System.out.println("Connecting to database..."); // Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); //STEP 4: 執(zhí)行一個查詢 System.out.println("Creating statement..."); // Statement stmt = conn.createStatement(); // ResultSet rs = stmt.executeQuery(sql); //STEP 5: 從結(jié)果集中提取數(shù)據(jù) while (rs.next()) { // 根據(jù)列名獲取數(shù)據(jù) int id = rs.getInt("id"); int age = rs.getInt("age"); String first = rs.getString("first"); String last = rs.getString("last"); // 顯示結(jié)果 System.out.print("ID: " + id); System.out.print(", Age: " + age); System.out.print(", First: " + first); System.out.println(", Last: " + last); } } catch (SQLException se) { // 處理可能出現(xiàn)的錯誤 se.printStackTrace(); } System.out.println("Goodbye!"); } }
JDBC的最佳實(shí)踐
- 數(shù)據(jù)庫資源是非常昂貴的,用完了應(yīng)該盡快關(guān)閉它。Connection, Statement, ResultSet等JDBC對象都有close方法,調(diào)用它就好了。
- 在代碼中必須顯式關(guān)閉掉ResultSet,Statement,Connection,如果你用的是連接池的話,連接用完后會放回池里,但是沒有關(guān)閉的ResultSet和Statement就會造成資源泄漏了。
- 在finally塊中關(guān)閉資源,保證即便出了異常也能正常關(guān)閉。
- 大量相似的查詢應(yīng)當(dāng)使用批處理完成。
- 盡量使用PreparedStatement而不是Statement,以避免SQL注入,同時還能通過預(yù)編譯和緩存機(jī)制提升執(zhí)行的效率。
- 如果你要將大量數(shù)據(jù)讀入到ResultSet中,應(yīng)該合理的設(shè)置fetchSize以便提升性能。
- 你用的數(shù)據(jù)庫可能沒有支持所有的隔離級別,用之前先仔細(xì)確認(rèn)下。
- 數(shù)據(jù)庫隔離級別越高性能越差,確保你的數(shù)據(jù)庫連接設(shè)置的隔離級別是最優(yōu)的。
- 如果你需要長時間對ResultSet進(jìn)行操作的話,盡量使用離線的RowSet。
FAQ
JDBC是如何實(shí)現(xiàn)Java程序和JDBC驅(qū)動的松耦合?
JDBC API使用Java的反射機(jī)制來實(shí)現(xiàn)Java程序和JDBC驅(qū)動的松耦合。看一下上文的JDBC示例,你會發(fā)現(xiàn)所有操作都是通過JDBC接口完成的,而驅(qū)動只有在通過Class.forName反射機(jī)制來加載的時候才會出現(xiàn)。
這是Java核心庫里反射機(jī)制的最佳實(shí)踐之一,它使得應(yīng)用程序和驅(qū)動程序之間進(jìn)行了隔離,讓遷移數(shù)據(jù)庫的工作變得更簡單。
Statement和PreparedStatement區(qū)別
- 關(guān)系:PreparedStatement繼承自Statement,兩者都是接口
- 區(qū)別:PreparedStatement可以使用占位符,而且是預(yù)編譯的,批處理比Statement效率高
預(yù)編譯
創(chuàng)建時的區(qū)別:
Statement statement = conn.createStatement(); PreparedStatement preStatement = conn.prepareStatement(sql);
執(zhí)行時的區(qū)別:
ResultSet rSet = statement.executeQuery(sql); ResultSet pSet = preStatement.executeQuery();
由上可以看出,PreparedStatement有預(yù)編譯的過程,已經(jīng)綁定sql,之后無論執(zhí)行多少次,都不會再去進(jìn)行編譯,而Statement 不同,如果執(zhí)行多次,則相應(yīng)的就要編譯多少次sql,所以從這點(diǎn)看,PreparedStatement的效率會比Statement要高一些。PreparedStatement是預(yù)編譯的,所以可以有效的防止SQL注入等問題
占位符
PrepareStatement可以替換變量在SQL語句中可以包含?,可以用?替換成變量。
ps = conn.prepareStatement("select * from Employees where id=?"); int sid = 1001; ps.setInt(1, sid); rs = ps.executeQuery();
而Statement只能用字符串拼接。
int sid = 1001; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select * from Employees where id=" + sid);
JDBC的ResultSet
在查詢數(shù)據(jù)庫后會返回一個ResultSet,它就像是查詢結(jié)果集的一張數(shù)據(jù)表。
ResultSet對象維護(hù)了一個游標(biāo),指向當(dāng)前的數(shù)據(jù)行。開始的時候這個游標(biāo)指向的是第一行。如果調(diào)用了ResultSet的next()方法游標(biāo)會下移一行,如果沒有更多的數(shù)據(jù)了,next()方法會返回false。可以在for循環(huán)中用它來遍歷數(shù)據(jù)集。
默認(rèn)的ResultSet是不能更新的,游標(biāo)也只能往下移。也就是說你只能從第一行到最后一行遍歷一遍。不過也可以創(chuàng)建可以回滾或者可更新的ResultSet,像下面這樣。
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
當(dāng)生成ResultSet的Statement對象要關(guān)閉或者重新執(zhí)行或是獲取下一個ResultSet的時候,ResultSet對象也會自動關(guān)閉。
可以通過ResultSet的getter方法,傳入列名或者從1開始的序號來獲取列數(shù)據(jù)。
ResultSet的不同類型
根據(jù)創(chuàng)建Statement時輸入?yún)?shù)的不同,會對應(yīng)不同類型的ResultSet。如果你看下Connection的方法,你會發(fā)現(xiàn)createStatement和prepareStatement方法重載了,以支持不同的ResultSet和并發(fā)類型。
ResultSet對象有三種類型。
- ResultSet.TYPE_FORWARD_ONLY:這是默認(rèn)的類型,它的游標(biāo)只能往下移。
- ResultSet.TYPE_SCROLL_INSENSITIVE:游標(biāo)可以上下移動,一旦它創(chuàng)建后,數(shù)據(jù)庫里的數(shù)據(jù)再發(fā)生修改,對它來說是透明的。
- ResultSet.TYPE_SCROLL_SENSITIVE:游標(biāo)可以上下移動,如果生成后數(shù)據(jù)庫還發(fā)生了修改操作,它是能夠感知到的。
ResultSet有兩種并發(fā)類型。
- ResultSet.CONCUR_READ_ONLY:ResultSet是只讀的,這是默認(rèn)類型。
- ResultSet.CONCUR_UPDATABLE:我們可以使用ResultSet的更新方法來更新里面的數(shù)據(jù)。
更多內(nèi)容,歡迎關(guān)注微信公眾號:全菜工程師小輝~