在Java語言中,有一個專門鏈接數據庫的規範(JDBC),專門負責鏈接數據庫進行數據操做。各個數據庫提供商會根據這套規範(接口)編寫相關的實現類,封裝成一個 jar 包供用戶下載使用。因此在進行編程時,須要將相應的 jar 包導入到工程文件下的 lib 目錄下,並創建依賴。java
這裏咱們使用的是 mysql 數據庫。mysql
經過下述語句實現註冊驅動,原理是這句語句會將 Driver.class 這份字節碼加載到 JVM 中,而後 JVM 會執行該字節碼的靜態代碼塊,mysql 提供的這個驅動包中,Driver 的字節碼內的靜態代碼塊就完成了驅動對象的建立和註冊。sql
1 //加載註冊驅動 2 Class.forName("com.mysql.jdbc.Driver");
當咱們註冊了驅動以後,能夠經過 DriverManager 獲取與數據庫的鏈接,須要傳入三個參數:數據庫的地址,登陸用戶名,登錄密碼。注意 Connection 和 DriverManager 類都是 java.sql 包下的,dbName 是數據庫的名字。數據庫
1 //鏈接數據庫 2 Connection conn = DriverManager.getConnection(jdbc:mysql://localhost:3306/dbName", "root", "123");
驗證已經獲取鏈接,能夠在 mysql 控制檯,使用命令:show processlist 查看運行進程。編程
當獲得與數據的鏈接以後,咱們還須要經過該鏈接得到一個語句對象,該語句對象內包含咱們要對數據庫執行的操做,也就是 SQL 語句。咱們執行 SQL 語句通常使用語句對象的 executeUpdate 和 executeQuery 方法,前者用於 DDL 和 DML 操做,返回收影響的行數;後者用於執行 DQL 操做,返回結果集對象。具體使用方法以下瀏覽器
1 //語句對象的得到 2 Statement st = conn.createStatement(); 3 //執行語句,這裏的語句用於建表 4 st.executeUpdate("create table t_student (id int primary key auto_increment, name varchar(20), age int)") 5 //釋放資源,先進後出,須要處理異常 6 st.close(); 7 conn.close();
執行一次語句的步驟能夠歸爲「賈(加)璉(連)欲(語)執(執)事(釋)」這一句話,合在一塊兒的源碼爲服務器
1 public class CreateTableTest { 2 public static void main(String[] args) throws ClassNotFoundException, SQLException { 3 Class.forName("com.mysql.jdbc.Driver"); 4 Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/jdbc_demo", "root", "1234"); 5 String sql = "create table t_student (id int primary key auto_increment, name varchar(20), age int)"; 6 7 Statement st = conn.createStatement(); 8 st.execute(sql); 9 10 st.close(); 11 conn.close(); 12 } 13 }
實際開發中,JavaWeb 開發代碼通常分爲三層,分層結構是 JavaWeb 開發中的一種設計思想,這樣會讓咱們開發井井有條,每一層只要完成對應的功能便可,使得項目便於開發和維護。工具
DAO : Data Access Object 數據訪問對象
在實際開發中,能夠按照下列結構進行設計性能
在 1.4 中示範了怎麼鏈接數據庫進行一次建表操做,實際上 DAO 中的實現類內就是一個個這樣的方法,好比常見的增、刪、改、查,若是每一個方法內都寫這樣一些列流程,會在後期維護中產生很大麻煩,假如鏈接密碼改了,每一個方法都要修改,這顯然不現實,因此咱們要利用工具類以及配置文檔優化代碼。測試
實現類中的方法應該專一於功能的實現,得到鏈接是該方法須要的一個結果,該方法並不關注這個過程,不該該放在方法內混淆語義。咱們能夠把鏈接數據庫的代碼放在 JdbcUtils 這個工具類內,該類的成員都是類成員,而後實現類的方法中直接經過類調用 getConnection() 方法得到鏈接。同時,註冊驅動這個步驟咱們只須要執行一次就夠了,咱們能夠把這個步驟放在工具類的靜態代碼塊中,在該類初始化的時候會自動執行一次。
1 public class JdbcUtils { 2 3 private static String driverClassName = "com.mysql.jdbc.Driver"; 4 private static String url = "jdbc:mysql://127.0.0.1:3306/jdbc_demo"; 5 private static String username = "root"; 6 private static String password = "1234"; 7 8 static { 9 try { 10 Class.forName(driverClassName); 11 } catch (ClassNotFoundException e) { 12 e.printStackTrace(); 13 } 14 } 15 16 public static Connection getConnection() throws SQLException { 17 Connection conn = DriverManager.getConnection(url, username, password); 18 return conn; 19 } 20 }
關閉資源也是一塊雞肋代碼,重複且冗長,未重構前每一個方法的關閉過程以下
1 //假設是查詢方法,除了關閉鏈接、語句還要關閉結果集,每次關閉都須要異常處理 2 /* 3 Connection conn; 4 PreparedStatement ps; 5 ResultSet rs; 6 */ 7 try { 8 //方法須要實現的功能 9 return xxx; 10 } catch (Exception e) { 11 e.printStackTrace(); 12 } finally { 13 //try 代碼塊中的 return 語句必定會在 finally 執行完後執行,因此關閉系統資源放在 finally 中。先進後出 14 try { 15 if(rs != null) { 16 rs.close(); 17 } 18 } catch (SQLException e) { 19 e.printStackTrace(); 20 } finally { 21 try { 22 if(ps != null) { 23 ps.close(); 24 } 25 } catch (SQLException e) { 26 e.printStackTrace(); 27 } finally { 28 try { 29 if(conn != null) { 30 conn.close(); 31 } 32 } catch (SQLException e) { 33 e.printStackTrace(); 34 } 35 } 36 } 37 }
能夠看到這一段關閉資源的方法很是冗長,且每一個方法都要關閉一遍。咱們能夠把這個關閉的流程寫進工具類內,實現類的方法只須要調用 JdbcUtils.close() 方法便可。
1 //Statement 是 PreparedStatement 的父類,這樣不論是 Statement 或者 PreparedStatement 類型的均可以用這個方法關閉 2 //對於 DML 語句,沒有 ResultSet 對象,能夠第三個變量穿 null 便可 3 public static void close(Connection conn, Statement s, ResultSet rs) { 4 try { 5 if(rs != null) { 6 rs.close(); 7 } 8 } catch (Exception e) { 9 e.printStackTrace(); 10 } finally { 11 try { 12 if(s != null) { 13 s.close(); 14 } 15 } catch (Exception e) { 16 e.printStackTrace(); 17 } finally { 18 try { 19 if(conn != null) { 20 conn.close(); 21 } 22 } catch (Exception e) { 23 e.printStackTrace(); 24 } 25 } 26 } 27 }
把鏈接須要的這些信息放在代碼塊中仍是不太方便,若是用戶修改了帳號密碼,或者主機地址改變,都須要從新修改代碼,這不太知足咱們日常應用的要求。因此咱們能夠把這些信息放在一個 .properties 文件中,Java 程序直接去讀取這些信息。那之後修改了帳戶密碼,只須要本身打開這個 .properties 文件修改相應的字段便可。
#key=value driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://127.0.0.1:3306/jdbc_demo username=root password=1234
.properties 內存放了一些鍵值對,注意等號左右沒有空格,結尾也沒有標點,# 做爲註釋
那如何讀取這些信息呢?
Java 中經過類加載器讀取配置文件得到一個輸入流,能夠經過兩種方法得到類加載器
1 //1.經過某一類的字節碼實例能夠獲取 2 ClassLoader cl = Object.class.getContextClassLoader(); 3 //2.經過當前線程得到 4 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 5 6 //經過類加載器讀取配置文件得到輸入流 7 InputStream in = cl.getResourceAsStream("dp.properties");
那如何得到相應的信息呢?
咱們能夠經過 Properties 對象得到相應的信息。Properties 類是 Map 抽象類下一個專門用於讀取配置文件的類,將輸入流加載進去,便可經過「key」得到「value」。
1 Properties p = new Properties(); 2 p.load(in); 3 System.out.println(p.getProperty("driverClassName"));
1 public class JdbcUtils { 2 3 private static Properties p = new Properties(); 4 5 static { 6 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 7 InputStream in = cl.getResourceAsStream("dp.properties"); 8 try { 9 p.load(in); 10 Class.forName(p.getProperty("driverClassName")); 11 } catch (Exception e) { 12 e.printStackTrace(); 13 } 14 } 15 16 public static Connection getConnection() throws SQLException { 17 Connection conn = DriverManager.getConnection(p.getProperty("url"), p.getProperty("username"), p.getProperty("password")); 18 return conn; 19 } 20 21 public static void close(Connection conn, Statement s, ResultSet rs) { 22 try { 23 if(rs != null) { 24 rs.close(); 25 } 26 } catch (Exception e) { 27 e.printStackTrace(); 28 } finally { 29 try { 30 if(s != null) { 31 s.close(); 32 } 33 } catch (Exception e) { 34 e.printStackTrace(); 35 } finally { 36 try { 37 if(conn != null) { 38 conn.close(); 39 } 40 } catch (Exception e) { 41 e.printStackTrace(); 42 } 43 } 44 } 45 } 46 }
按照上面的作法,每次用戶進行一次操做,都會創建鏈接,接着執行完成後銷燬鏈接。雖然每次鏈接、斷開鏈接用時不長,但當用戶數量上來意後,對系統資源的消耗就會變得很高。因而咱們引入鏈接池管理鏈接。鏈接池裏面擁有必定數量的鏈接(通常5 - 10個),當經過鏈接池getConnection 時,鏈接池提供一個鏈接供方法使用,當使用完畢後方法執行鏈接的 close 方法,這個時候並非直接關閉鏈接,而是將鏈接返回給鏈接池。
在Java中,鏈接池使用 javax.sql.DataSource 接口來表示鏈接池。注意:DataSource 僅僅只是一個接口,由各大服務器廠商來實現。經常使用的 DataSource 的實現:
介紹 Druid 使用方法
1 //須要導入 Druid 的 jar 包 2 //方法一: 3 //1 建立鏈接池對象,不能使用 DataSource 類型,由於 setXxx 方法時 DruidDataSource 類獨有的 4 DruidDataSource dds = new DruidDataSource(); 5 //2 設置鏈接數據庫的信息 6 dds.setDriverClassName(p.getProperty("driverClassName")); 7 dds.setUrl(p.getProperty("url")); 8 dds.setUsername(p.getProperty("username")); 9 dds.setPassword(p.getProperty("password")); 10 dds.setMaxActive(10); //最大鏈接數 11 Connection conn = dds.getConnection(); 12 13 //方法二 14 //經過鏈接池工程得到鏈接池 15 //1 經過工廠的靜態方法得到鏈接池,傳入上文中的 Properties 對象做爲參數,工程自動讀取配置信息 16 DataSource ds = DruidDataSourceFactory.createDataSource(p); 17 //2 得到鏈接 18 Connection conn = ds.getConnection();
使用鏈接池後的工具類
1 public class JdbcUtils { 2 3 private static Properties p = new Properties(); 4 5 //用工廠建立鏈接池對象,工廠底層對 properties 直接讀取,只需傳入一個 Properties 對象 6 private static DataSource dds; 7 static { 8 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 9 InputStream in = cl.getResourceAsStream("dp.properties"); 10 try { 11 p.load(in); 12 dds = DruidDataSourceFactory.createDataSource(p); 13 } catch (Exception e) { 14 e.printStackTrace(); 15 } 16 } 17 18 public static Connection getConnection() throws SQLException { 19 return dds.getConnection(); 20 } 21 22 public static void close(Connection conn, Statement s, ResultSet rs) { 23 try { 24 if(rs != null) { 25 rs.close(); 26 } 27 } catch (Exception e) { 28 e.printStackTrace(); 29 } finally { 30 try { 31 if(s != null) { 32 s.close(); 33 } 34 } catch (Exception e) { 35 e.printStackTrace(); 36 } finally { 37 try { 38 if(conn != null) { 39 conn.close(); 40 } 41 } catch (Exception e) { 42 e.printStackTrace(); 43 } 44 } 45 } 46 } 47 }
JDBC 中是默認提交事務的,也就是每一條 Statement 執行後,自動提交事務。在實際業務中,咱們可能須要把多個操做看成一個原子性操做來進行,要麼全作,要麼所有作。要實現這個要求,咱們只須要把自動提交事務給關閉,在經過回滾(rollback)和提交(commit)來完成一次事務。
1 //銀行轉帳例子 2 public class TransactionTest { 3 @Test 4 public void testName() throws Exception { 5 Connection conn = null; 6 Statement st = null; 7 ResultSet rs = null; 8 try { 9 conn = DruidUtil.getConnection(); 10 //將事務設置爲手動提交 11 conn.setAutoCommit(false); 12 13 st = conn.createStatement(); 14 // 1.檢查張無忌的帳號餘額是否大於等於1000. 15 rs = st.executeQuery("SELECT balance FROM account WHERE name = '張無忌' AND balance >=1000"); 16 if(!rs.next()) { 17 throw new RuntimeException("親,您的帳戶餘額不夠"); 18 } 19 // 餘額>=1000:GOTO 2: 20 // 餘額 <1000:提示:親,你的餘額不足. 21 // 2.在張無忌的帳號餘額上減小1000. 22 st.executeUpdate("UPDATE account SET balance = balance-1000 WHERE name = '張無忌'"); 23 24 System.out.println(1/0); //製造異常 25 26 // 3.在趙敏的帳戶餘額尚增長1000. 27 st.executeUpdate("UPDATE account SET balance = balance+1000 WHERE name = '趙敏'"); 28 29 //提交事務 30 conn.commit(); 31 } catch (Exception e) { 32 e.printStackTrace(); 33 //回滾事務 34 conn.rollback(); 35 }finally { 36 DruidUtil.close(conn, st, rs); 37 } 38 } 39 } 40 //最終錢不會由於異常而變少