【JDBC】總結

JDBC核心技術java

第1章:JDBC概述

1.1 數據的持久化

  • 持久化(persistence):把數據保存到可掉電式存儲設備中以供以後使用。大多數狀況下,特別是企業級應用,數據持久化意味着將內存中的數據保存到硬盤上加以」固化」,而持久化的實現過程大多經過各類關係數據庫來完成mysql

  • 持久化的主要應用是將內存中的數據存儲在關係型數據庫中,固然也能夠存儲在磁盤文件、XML數據文件中。git

1.2 Java中的數據存儲技術

  • 在Java中,數據庫存取技術可分爲以下幾類:
    • JDBC直接訪問數據庫
    • JDO (Java Data Object )技術程序員

    • 第三方O/R工具,如Hibernate, Mybatis 等github

  • JDBC是java訪問數據庫的基石,JDO、Hibernate、MyBatis等只是更好的封裝了JDBC。

1.3 JDBC介紹

  • JDBC(Java Database Connectivity)是一個獨立於特定數據庫管理系統、通用的SQL數據庫存取和操做的公共接口(一組API),定義了用來訪問數據庫的標準Java類庫,(java.sql,javax.sql)使用這些類庫能夠以一種標準的方法、方便地訪問數據庫資源。
  • JDBC爲訪問不一樣的數據庫提供了一種統一的途徑,爲開發者屏蔽了一些細節問題。
  • JDBC的目標是使Java程序員使用JDBC能夠鏈接任何提供了JDBC驅動程序的數據庫系統,這樣就使得程序員無需對特定的數據庫系統的特色有過多的瞭解,從而大大簡化和加快了開發過程。
  • 若是沒有JDBC,那麼Java程序訪問數據庫時是這樣的:


  • 有了JDBC,Java程序訪問數據庫時是這樣的:


  • 總結以下:

1.4 JDBC體系結構

  • JDBC接口(API)包括兩個層次:
    • 面向應用的API:Java API,抽象接口,供應用程序開發人員使用(鏈接數據庫,執行SQL語句,得到結果)。
    • 面向數據庫的API:Java Driver API,供開發商開發數據庫驅動程序用。

JDBC是sun公司提供一套用於數據庫操做的接口,java程序員只須要面向這套接口編程便可。web

不一樣的數據庫廠商,須要針對這套接口,提供不一樣實現。不一樣的實現的集合,即爲不一樣數據庫的驅動。 ————面向接口編程sql

1.5 JDBC程序編寫步驟

補充:ODBC(Open Database Connectivity,開放式數據庫鏈接),是微軟在Windows平臺下推出的。使用者在程序中只須要調用ODBC API,由 ODBC 驅動程序將調用轉換成爲對特定的數據庫的調用請求。數據庫

第2章:獲取數據庫鏈接

2.1 要素一:Driver接口實現類

2.1.1 Driver接口介紹

  • java.sql.Driver 接口是全部 JDBC 驅動程序須要實現的接口。這個接口是提供給數據庫廠商使用的,不一樣數據庫廠商提供不一樣的實現。apache

  • 在程序中不須要直接去訪問實現了 Driver 接口的類,而是由驅動程序管理器類(java.sql.DriverManager)去調用這些Driver實現。
    • Oracle的驅動:oracle.jdbc.driver.OracleDriver
    • mySql的驅動: com.mysql.jdbc.Driver

  • 將上述jar包拷貝到Java工程的一個目錄中,習慣上新建一個lib文件夾。

在驅動jar上右鍵-->Build Path-->Add to Build Path編程

注意:若是是Dynamic Web Project(動態的web項目)話,則是把驅動jar放到WebContent(有的開發工具叫WebRoot)目錄中的WEB-INF目錄中的lib目錄下便可

2.1.2 加載與註冊JDBC驅動

  • 加載驅動:加載 JDBC 驅動需調用 Class 類的靜態方法 forName(),向其傳遞要加載的 JDBC 驅動的類名

    • Class.forName(「com.mysql.jdbc.Driver」);
  • 註冊驅動:DriverManager 類是驅動程序管理器類,負責管理驅動程序
    • 使用DriverManager.registerDriver(com.mysql.jdbc.Driver)來註冊驅動

    • 一般不用顯式調用 DriverManager 類的 registerDriver() 方法來註冊驅動程序類的實例,由於 Driver 接口的驅動程序類包含了靜態代碼塊,在這個靜態代碼塊中,會調用 DriverManager.registerDriver() 方法來註冊自身的一個實例。下圖是MySQL的Driver實現類的源碼:

2.2 要素二:URL

  • JDBC URL 用於標識一個被註冊的驅動程序,驅動程序管理器經過這個 URL 選擇正確的驅動程序,從而創建到數據庫的鏈接。

  • JDBC URL的標準由三部分組成,各部分間用冒號分隔。
    • jdbc:子協議:子名稱
    • 協議:JDBC URL中的協議老是jdbc
    • 子協議:子協議用於標識一個數據庫驅動程序
    • 子名稱:一種標識數據庫的方法。子名稱能夠依不一樣的子協議而變化,用子名稱的目的是爲了定位數據庫提供足夠的信息。包含主機名(對應服務端的ip地址),端口號,數據庫名
  • 舉例:

  • 幾種經常使用數據庫的 JDBC URL

    • MySQL的鏈接URL編寫方式:

      • jdbc:mysql://主機名稱:mysql服務端口號/數據庫名稱?參數=值&參數=值
      • jdbc:mysql://localhost:3306/atguigu
      • jdbc:mysql://localhost:3306/atguigu?useUnicode=true&characterEncoding=utf8(若是JDBC程序與服務器端的字符集不一致,會致使亂碼,那麼能夠經過參數指定服務器端的字符集)
      • jdbc:mysql://localhost:3306/atguigu?user=root&password=123456
    • Oracle 9i的鏈接URL編寫方式:

      • jdbc:oracle:thin:@主機名稱:oracle服務端口號:數據庫名稱
      • jdbc:oracle:thin:@localhost:1521:atguigu
    • SQLServer的鏈接URL編寫方式:

      • jdbc:sqlserver://主機名稱:sqlserver服務端口號:DatabaseName=數據庫名稱

      • jdbc:sqlserver://localhost:1433:DatabaseName=atguigu

2.3 要素三:用戶名和密碼

  • user,password能夠用「屬性名=屬性值」方式告訴數據庫
  • 能夠調用 DriverManager 類的 getConnection() 方法創建到數據庫的鏈接

2.4 數據庫鏈接方式舉例

2.4.1 鏈接方式一

@Test
    public void testConnection1() {
        try {
            //1.提供java.sql.Driver接口實現類的對象
            Driver driver = null;
            driver = new com.mysql.jdbc.Driver();

            //2.提供url,指明具體操做的數據
            String url = "jdbc:mysql://localhost:3306/test";

            //3.提供Properties的對象,指明用戶名和密碼
            Properties info = new Properties();
            info.setProperty("user", "root");
            info.setProperty("password", "abc123");

            //4.調用driver的connect(),獲取鏈接
            Connection conn = driver.connect(url, info);
            System.out.println(conn);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

說明:上述代碼中顯式出現了第三方數據庫的API

2.4.2 鏈接方式二

@Test
    public void testConnection2() {
        try {
            //1.實例化Driver
            String className = "com.mysql.jdbc.Driver";
            Class clazz = Class.forName(className);
            Driver driver = (Driver) clazz.newInstance();

            //2.提供url,指明具體操做的數據
            String url = "jdbc:mysql://localhost:3306/test";

            //3.提供Properties的對象,指明用戶名和密碼
            Properties info = new Properties();
            info.setProperty("user", "root");
            info.setProperty("password", "abc123");

            //4.調用driver的connect(),獲取鏈接
            Connection conn = driver.connect(url, info);
            System.out.println(conn);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

說明:相較於方式一,這裏使用反射實例化Driver,不在代碼中體現第三方數據庫的API。體現了面向接口編程思想。

2.4.3 鏈接方式三

@Test
    public void testConnection3() {
        try {
            //1.數據庫鏈接的4個基本要素:
            String url = "jdbc:mysql://localhost:3306/test";
            String user = "root";
            String password = "abc123";
            String driverName = "com.mysql.jdbc.Driver";

            //2.實例化Driver
            Class clazz = Class.forName(driverName);
            Driver driver = (Driver) clazz.newInstance();
            //3.註冊驅動
            DriverManager.registerDriver(driver);
            //4.獲取鏈接
            Connection conn = DriverManager.getConnection(url, user, password);
            System.out.println(conn);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

說明:使用DriverManager實現數據庫的鏈接。體會獲取鏈接必要的4個基本要素。

2.4.4 鏈接方式四

@Test
    public void testConnection4() {
        try {
            //1.數據庫鏈接的4個基本要素:
            String url = "jdbc:mysql://localhost:3306/test";
            String user = "root";
            String password = "abc123";
            String driverName = "com.mysql.jdbc.Driver";

            //2.加載驅動 (①實例化Driver ②註冊驅動)
            Class.forName(driverName);


            //Driver driver = (Driver) clazz.newInstance();
            //3.註冊驅動
            //DriverManager.registerDriver(driver);
            /*
            能夠註釋掉上述代碼的緣由,是由於在mysql的Driver類中聲明有:
            static {
                try {
                    DriverManager.registerDriver(new Driver());
                } catch (SQLException var1) {
                    throw new RuntimeException("Can't register driver!");
                }
            }

             */


            //3.獲取鏈接
            Connection conn = DriverManager.getConnection(url, user, password);
            System.out.println(conn);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

說明:沒必要顯式的註冊驅動了。由於在DriverManager的源碼中已經存在靜態代碼塊,實現了驅動的註冊。

2.4.5 鏈接方式五(最終版)

@Test
    public  void testConnection5() throws Exception {
        //1.加載配置文件
        InputStream is = ConnectionTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
        Properties pros = new Properties();
        pros.load(is);
        
        //2.讀取配置信息
        String user = pros.getProperty("user");
        String password = pros.getProperty("password");
        String url = pros.getProperty("url");
        String driverClass = pros.getProperty("driverClass");

        //3.加載驅動
        Class.forName(driverClass);

        //4.獲取鏈接
        Connection conn = DriverManager.getConnection(url,user,password);
        System.out.println(conn);

    }

其中,配置文件聲明在工程的src目錄下:【jdbc.properties】

user=root
password=abc123
url=jdbc:mysql://localhost:3306/test
driverClass=com.mysql.jdbc.Driver

說明:使用配置文件的方式保存配置信息,在代碼中加載配置文件

使用配置文件的好處:

①實現了代碼和數據的分離,若是須要修改配置信息,直接在配置文件中修改,不須要深刻代碼
②若是修改了配置信息,省去從新編譯的過程。

第3章:使用PreparedStatement實現CRUD操做

3.1 操做和訪問數據庫

  • 數據庫鏈接被用於向數據庫服務器發送命令和 SQL 語句,並接受數據庫服務器返回的結果。其實一個數據庫鏈接就是一個Socket鏈接。

  • 在 java.sql 包中有 3 個接口分別定義了對數據庫的調用的不一樣方式:
    • Statement:用於執行靜態 SQL 語句並返回它所生成結果的對象。
    • PrepatedStatement:SQL 語句被預編譯並存儲在此對象中,可使用此對象屢次高效地執行該語句。
    • CallableStatement:用於執行 SQL 存儲過程

3.2 使用Statement操做數據表的弊端

  • 經過調用 Connection 對象的 createStatement() 方法建立該對象。該對象用於執行靜態的 SQL 語句,而且返回執行結果。

  • Statement 接口中定義了下列方法用於執行 SQL 語句:

    int excuteUpdate(String sql):執行更新操做INSERT、UPDATE、DELETE
    ResultSet executeQuery(String sql):執行查詢操做SELECT
  • 可是使用Statement操做數據表存在弊端:

    • 問題一:存在拼串操做,繁瑣
    • 問題二:存在SQL注入問題
  • SQL 注入是利用某些系統沒有對用戶輸入的數據進行充分的檢查,而在用戶輸入數據中注入非法的 SQL 語句段或命令(如:SELECT user, password FROM user_table WHERE user='a' OR 1 = ' AND password = ' OR '1' = '1') ,從而利用系統的 SQL 引擎完成惡意行爲的作法。

  • 對於 Java 而言,要防範 SQL 注入,只要用 PreparedStatement(從Statement擴展而來) 取代 Statement 就能夠了。

  • 代碼演示:

public class StatementTest {

    // 使用Statement的弊端:須要拼寫sql語句,而且存在SQL注入的問題
    @Test
    public void testLogin() {
        Scanner scan = new Scanner(System.in);

        System.out.print("用戶名:");
        String userName = scan.nextLine();
        System.out.print("密   碼:");
        String password = scan.nextLine();

        // SELECT user,password FROM user_table WHERE USER = '1' or ' AND PASSWORD = '='1' or '1' = '1';
        String sql = "SELECT user,password FROM user_table WHERE USER = '" + userName + "' AND PASSWORD = '" + password
                + "'";
        User user = get(sql, User.class);
        if (user != null) {
            System.out.println("登錄成功!");
        } else {
            System.out.println("用戶名或密碼錯誤!");
        }
    }

    // 使用Statement實現對數據表的查詢操做
    public <T> T get(String sql, Class<T> clazz) {
        T t = null;

        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try {
            // 1.加載配置文件
            InputStream is = StatementTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
            Properties pros = new Properties();
            pros.load(is);

            // 2.讀取配置信息
            String user = pros.getProperty("user");
            String password = pros.getProperty("password");
            String url = pros.getProperty("url");
            String driverClass = pros.getProperty("driverClass");

            // 3.加載驅動
            Class.forName(driverClass);

            // 4.獲取鏈接
            conn = DriverManager.getConnection(url, user, password);

            st = conn.createStatement();

            rs = st.executeQuery(sql);

            // 獲取結果集的元數據
            ResultSetMetaData rsmd = rs.getMetaData();

            // 獲取結果集的列數
            int columnCount = rsmd.getColumnCount();

            if (rs.next()) {

                t = clazz.newInstance();

                for (int i = 0; i < columnCount; i++) {
                    // //1. 獲取列的名稱
                    // String columnName = rsmd.getColumnName(i+1);

                    // 1. 獲取列的別名
                    String columnName = rsmd.getColumnLabel(i + 1);

                    // 2. 根據列名獲取對應數據表中的數據
                    Object columnVal = rs.getObject(columnName);

                    // 3. 將數據表中獲得的數據,封裝進對象
                    Field field = clazz.getDeclaredField(columnName);
                    field.setAccessible(true);
                    field.set(t, columnVal);
                }
                return t;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 關閉資源
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (st != null) {
                try {
                    st.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }

            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }

        return null;
    }
}

綜上:

3.3 PreparedStatement的使用

3.3.1 PreparedStatement介紹

  • 能夠經過調用 Connection 對象的 preparedStatement(String sql) 方法獲取 PreparedStatement 對象

  • PreparedStatement 接口是 Statement 的子接口,它表示一條預編譯過的 SQL 語句

  • PreparedStatement 對象所表明的 SQL 語句中的參數用問號(?)來表示,調用 PreparedStatement 對象的 setXxx() 方法來設置這些參數. setXxx() 方法有兩個參數,第一個參數是要設置的 SQL 語句中的參數的索引(從 1 開始),第二個是設置的 SQL 語句中的參數的值

3.3.2 PreparedStatement vs Statement

  • 代碼的可讀性和可維護性。

  • PreparedStatement 能最大可能提升性能:
    • DBServer會對預編譯語句提供性能優化。由於預編譯語句有可能被重複調用,因此語句在被DBServer的編譯器編譯後的執行代碼被緩存下來,那麼下次調用時只要是相同的預編譯語句就不須要編譯,只要將參數直接傳入編譯過的語句執行代碼中就會獲得執行。
    • 在statement語句中,即便是相同操做但由於數據內容不同,因此整個語句自己不能匹配,沒有緩存語句的意義.事實是沒有數據庫會對普通語句編譯後的執行代碼緩存。這樣每執行一次都要對傳入的語句編譯一次。
    • (語法檢查,語義檢查,翻譯成二進制命令,緩存)
  • PreparedStatement 能夠防止 SQL 注入

3.3.3 Java與SQL對應數據類型轉換表

Java類型 SQL類型
boolean BIT
byte TINYINT
short SMALLINT
int INTEGER
long BIGINT
String CHAR,VARCHAR,LONGVARCHAR
byte array BINARY , VAR BINARY
java.sql.Date DATE
java.sql.Time TIME
java.sql.Timestamp TIMESTAMP

3.3.4 使用PreparedStatement實現增、刪、改操做

//通用的增、刪、改操做(體現一:增、刪、改 ; 體現二:針對於不一樣的表)
    public void update(String sql,Object ... args){
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            //1.獲取數據庫的鏈接
            conn = JDBCUtils.getConnection();
            
            //2.獲取PreparedStatement的實例 (或:預編譯sql語句)
            ps = conn.prepareStatement(sql);
            //3.填充佔位符
            for(int i = 0;i < args.length;i++){
                ps.setObject(i + 1, args[i]);
            }
            
            //4.執行sql語句
            ps.execute();
        } catch (Exception e) {
            
            e.printStackTrace();
        }finally{
            //5.關閉資源
            JDBCUtils.closeResource(conn, ps);
            
        }
    }

3.3.5 使用PreparedStatement實現查詢操做

// 通用的針對於不一樣表的查詢:返回一個對象 (version 1.0)
    public <T> T getInstance(Class<T> clazz, String sql, Object... args) {

        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // 1.獲取數據庫鏈接
            conn = JDBCUtils.getConnection();

            // 2.預編譯sql語句,獲得PreparedStatement對象
            ps = conn.prepareStatement(sql);

            // 3.填充佔位符
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);
            }

            // 4.執行executeQuery(),獲得結果集:ResultSet
            rs = ps.executeQuery();

            // 5.獲得結果集的元數據:ResultSetMetaData
            ResultSetMetaData rsmd = rs.getMetaData();

            // 6.1經過ResultSetMetaData獲得columnCount,columnLabel;經過ResultSet獲得列值
            int columnCount = rsmd.getColumnCount();
            if (rs.next()) {
                T t = clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {// 遍歷每個列

                    // 獲取列值
                    Object columnVal = rs.getObject(i + 1);
                    // 獲取列的別名:列的別名,使用類的屬性名充當
                    String columnLabel = rsmd.getColumnLabel(i + 1);
                    // 6.2使用反射,給對象的相應屬性賦值
                    Field field = clazz.getDeclaredField(columnLabel);
                    field.setAccessible(true);
                    field.set(t, columnVal);

                }

                return t;

            }
        } catch (Exception e) {

            e.printStackTrace();
        } finally {
            // 7.關閉資源
            JDBCUtils.closeResource(conn, ps, rs);
        }

        return null;

    }

說明:使用PreparedStatement實現的查詢操做能夠替換Statement實現的查詢操做,解決Statement拼串和SQL注入問題。

3.4 ResultSet與ResultSetMetaData

3.4.1 ResultSet

  • 查詢須要調用PreparedStatement 的 executeQuery() 方法,查詢結果是一個ResultSet 對象

  • ResultSet 對象以邏輯表格的形式封裝了執行數據庫操做的結果集,ResultSet 接口由數據庫廠商提供實現
  • ResultSet 返回的實際上就是一張數據表。有一個指針指向數據表的第一條記錄的前面。

  • ResultSet 對象維護了一個指向當前數據行的遊標,初始的時候,遊標在第一行以前,能夠經過 ResultSet 對象的 next() 方法移動到下一行。調用 next()方法檢測下一行是否有效。如有效,該方法返回 true,且指針下移。至關於Iterator對象的 hasNext() 和 next() 方法的結合體。
  • 當指針指向一行時, 能夠經過調用 getXxx(int index) 或 getXxx(int columnName) 獲取每一列的值。

    • 例如: getInt(1), getString("name")
    • 注意:Java與數據庫交互涉及到的相關Java API中的索引都從1開始。
  • ResultSet 接口的經常使用方法:
    • boolean next()

    • getString()

3.4.2 ResultSetMetaData

  • 可用於獲取關於 ResultSet 對象中列的類型和屬性信息的對象

  • ResultSetMetaData meta = rs.getMetaData();
    • getColumnName(int column):獲取指定列的名稱
    • getColumnLabel(int column):獲取指定列的別名
    • getColumnCount():返回當前 ResultSet 對象中的列數。

    • getColumnTypeName(int column):檢索指定列的數據庫特定的類型名稱。
    • getColumnDisplaySize(int column):指示指定列的最大標準寬度,以字符爲單位。
    • isNullable(int column):指示指定列中的值是否能夠爲 null。

    • isAutoIncrement(int column):指示是否自動爲指定列進行編號,這樣這些列仍然是隻讀的。

問題1:獲得結果集後, 如何知道該結果集中有哪些列 ? 列名是什麼?

​ 須要使用一個描述 ResultSet 的對象, 即 ResultSetMetaData

問題2:關於ResultSetMetaData

  1. 如何獲取 ResultSetMetaData: 調用 ResultSet 的 getMetaData() 方法便可
  2. 獲取 ResultSet 中有多少列:調用 ResultSetMetaData 的 getColumnCount() 方法
  3. 獲取 ResultSet 每一列的列的別名是什麼:調用 ResultSetMetaData 的getColumnLabel() 方法

3.5 資源的釋放

  • 釋放ResultSet, Statement,Connection。
  • 數據庫鏈接(Connection)是很是稀有的資源,用完後必須立刻釋放,若是Connection不能及時正確的關閉將致使系統宕機。Connection的使用原則是儘可能晚建立,儘可能早的釋放。
  • 能夠在finally中關閉,保證及時其餘代碼出現異常,資源也必定能被關閉。

3.6 JDBC API小結

  • 兩種思想
    • 面向接口編程的思想

    • ORM思想(object relational mapping)
      • 一個數據表對應一個java類
      • 表中的一條記錄對應java類的一個對象
      • 表中的一個字段對應java類的一個屬性

    sql是須要結合列名和表的屬性名來寫。注意起別名。

  • 兩種技術
    • JDBC結果集的元數據:ResultSetMetaData
      • 獲取列數:getColumnCount()
      • 獲取列的別名:getColumnLabel()
    • 經過反射,建立指定類的對象,獲取指定的屬性並賦值

章節練習

練習題1:從控制檯向數據庫的表customers中插入一條數據,表結構以下:

練習題2:創立數據庫表 examstudent,表結構以下:

向數據表中添加以下數據:

代碼實現1:插入一個新的student 信息

請輸入考生的詳細信息

Type:
IDCard:
ExamCard:
StudentName:
Location:
Grade:

信息錄入成功!

代碼實現2:在 eclipse中創建 java 程序:輸入身份證號或准考證號能夠查詢到學生的基本信息。結果以下:

代碼實現3:完成學生信息的刪除功能


第4章:操做BLOB類型字段

4.1 MySQL BLOB類型

  • MySQL中,BLOB是一個二進制大型對象,是一個能夠存儲大量數據的容器,它能容納不一樣大小的數據。
  • 插入BLOB類型的數據必須使用PreparedStatement,由於BLOB類型的數據沒法使用字符串拼接寫的。

  • MySQL的四種BLOB類型(除了在存儲的最大信息量上不一樣外,他們是等同的)

  • 實際使用中根據須要存入的數據大小定義不一樣的BLOB類型。
  • 須要注意的是:若是存儲的文件過大,數據庫的性能會降低。
  • 若是在指定了相關的Blob類型之後,還報錯:xxx too large,那麼在mysql的安裝目錄下,找my.ini文件加上以下的配置參數: max_allowed_packet=16M。同時注意:修改了my.ini文件以後,須要從新啓動mysql服務。

4.2 向數據表中插入大數據類型

//獲取鏈接
Connection conn = JDBCUtils.getConnection();
        
String sql = "insert into customers(name,email,birth,photo)values(?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);

// 填充佔位符
ps.setString(1, "徐海強");
ps.setString(2, "xhq@126.com");
ps.setDate(3, new Date(new java.util.Date().getTime()));
// 操做Blob類型的變量
FileInputStream fis = new FileInputStream("xhq.png");
ps.setBlob(4, fis);
//執行
ps.execute();
        
fis.close();
JDBCUtils.closeResource(conn, ps);

4.3 修改數據表中的Blob類型字段

Connection conn = JDBCUtils.getConnection();
String sql = "update customers set photo = ? where id = ?";
PreparedStatement ps = conn.prepareStatement(sql);

// 填充佔位符
// 操做Blob類型的變量
FileInputStream fis = new FileInputStream("coffee.png");
ps.setBlob(1, fis);
ps.setInt(2, 25);

ps.execute();

fis.close();
JDBCUtils.closeResource(conn, ps);

4.4 從數據表中讀取大數據類型

String sql = "SELECT id, name, email, birth, photo FROM customer WHERE id = ?";
conn = getConnection();
ps = conn.prepareStatement(sql);
ps.setInt(1, 8);
rs = ps.executeQuery();
if(rs.next()){
    Integer id = rs.getInt(1);
    String name = rs.getString(2);
    String email = rs.getString(3);
    Date birth = rs.getDate(4);
    Customer cust = new Customer(id, name, email, birth);
    System.out.println(cust); 
    //讀取Blob類型的字段
    Blob photo = rs.getBlob(5);
    InputStream is = photo.getBinaryStream();
    OutputStream os = new FileOutputStream("c.jpg");
    byte [] buffer = new byte[1024];
    int len = 0;
    while((len = is.read(buffer)) != -1){
        os.write(buffer, 0, len);
    }
    JDBCUtils.closeResource(conn, ps, rs);
        
    if(is != null){
        is.close();
    }
        
    if(os !=  null){
        os.close();
    }
    
}

第5章:批量插入

5.1 批量執行SQL語句

當須要成批插入或者更新記錄時,能夠採用Java的批量更新機制,這一機制容許多條語句一次性提交給數據庫批量處理。一般狀況下比單獨提交處理更有效率

JDBC的批量處理語句包括下面三個方法:

  • addBatch(String):添加須要批量處理的SQL語句或是參數;
  • executeBatch():執行批量處理語句;
  • clearBatch():清空緩存的數據

一般咱們會遇到兩種批量執行SQL語句的狀況:

  • 多條SQL語句的批量處理;
  • 一個SQL語句的批量傳參;

5.2 高效的批量插入

舉例:向數據表中插入20000條數據

  • 數據庫中提供一個goods表。建立以下:
CREATE TABLE goods(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20)
);

5.2.1 實現層次一:使用Statement

Connection conn = JDBCUtils.getConnection();
Statement st = conn.createStatement();
for(int i = 1;i <= 20000;i++){
    String sql = "insert into goods(name) values('name_' + "+ i +")";
    st.executeUpdate(sql);
}

5.2.2 實現層次二:使用PreparedStatement

long start = System.currentTimeMillis();
        
Connection conn = JDBCUtils.getConnection();
        
String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);
for(int i = 1;i <= 20000;i++){
    ps.setString(1, "name_" + i);
    ps.executeUpdate();
}
        
long end = System.currentTimeMillis();
System.out.println("花費的時間爲:" + (end - start));//82340
        
        
JDBCUtils.closeResource(conn, ps);

5.2.3 實現層次三

/*
 * 修改1: 使用 addBatch() / executeBatch() / clearBatch()
 * 修改2:mysql服務器默認是關閉批處理的,咱們須要經過一個參數,讓mysql開啓批處理的支持。
 *       ?rewriteBatchedStatements=true 寫在配置文件的url後面
 * 修改3:使用更新的mysql 驅動:mysql-connector-java-5.1.37-bin.jar
 * 
 */
@Test
public void testInsert1() throws Exception{
    long start = System.currentTimeMillis();
        
    Connection conn = JDBCUtils.getConnection();
        
    String sql = "insert into goods(name)values(?)";
    PreparedStatement ps = conn.prepareStatement(sql);
        
    for(int i = 1;i <= 1000000;i++){
        ps.setString(1, "name_" + i);
            
        //1.「攢」sql
        ps.addBatch();
        if(i % 500 == 0){
            //2.執行
            ps.executeBatch();
            //3.清空
            ps.clearBatch();
        }
    }
        
    long end = System.currentTimeMillis();
    System.out.println("花費的時間爲:" + (end - start));//20000條:625                                                                         //1000000條:14733  
        
    JDBCUtils.closeResource(conn, ps);
}

5.2.4 實現層次四

/*
* 層次四:在層次三的基礎上操做
* 使用Connection 的 setAutoCommit(false)  /  commit()
*/
@Test
public void testInsert2() throws Exception{
    long start = System.currentTimeMillis();
        
    Connection conn = JDBCUtils.getConnection();
        
    //1.設置爲不自動提交數據
    conn.setAutoCommit(false);
        
    String sql = "insert into goods(name)values(?)";
    PreparedStatement ps = conn.prepareStatement(sql);
        
    for(int i = 1;i <= 1000000;i++){
        ps.setString(1, "name_" + i);
            
        //1.「攢」sql
        ps.addBatch();
            
        if(i % 500 == 0){
            //2.執行
            ps.executeBatch();
            //3.清空
            ps.clearBatch();
        }
    }
        
    //2.提交數據
    conn.commit();
        
    long end = System.currentTimeMillis();
    System.out.println("花費的時間爲:" + (end - start));//1000000條:4978 
        
    JDBCUtils.closeResource(conn, ps);
}

第6章:數據庫事務

6.1 數據庫事務介紹

  • 事務:一組邏輯操做單元,使數據從一種狀態變換到另外一種狀態。

  • 事務處理(事務操做):保證全部事務都做爲一個工做單元來執行,即便出現了故障,都不能改變這種執行方式。當在一個事務中執行多個操做時,要麼全部的事務都被提交(commit),那麼這些修改就永久地保存下來;要麼數據庫管理系統將放棄所做的全部修改,整個事務回滾(rollback)到最初狀態。

  • 爲確保數據庫中數據的一致性,數據的操縱應當是離散的成組的邏輯單元:當它所有完成時,數據的一致性能夠保持,而當這個單元中的一部分操做失敗,整個事務應所有視爲錯誤,全部從起始點之後的操做應所有回退到開始狀態。

6.2 JDBC事務處理

  • 數據一旦提交,就不可回滾。
  • 數據何時意味着提交?
    • 當一個鏈接對象被建立時,默認狀況下是自動提交事務:每次執行一個 SQL 語句時,若是執行成功,就會向數據庫自動提交,而不能回滾。
    • 關閉數據庫鏈接,數據就會自動的提交。若是多個操做,每一個操做使用的是本身單獨的鏈接,則沒法保證事務。即同一個事務的多個操做必須在同一個鏈接下。
  • JDBC程序中爲了讓多個 SQL 語句做爲一個事務執行:

    • 調用 Connection 對象的 setAutoCommit(false); 以取消自動提交事務
    • 在全部的 SQL 語句都成功執行後,調用 commit(); 方法提交事務
    • 在出現異常時,調用 rollback(); 方法回滾事務

    若此時 Connection 沒有被關閉,還可能被重複使用,則須要恢復其自動提交狀態 setAutoCommit(true)。尤爲是在使用數據庫鏈接池技術時,執行close()方法前,建議恢復自動提交狀態。

【案例:用戶AA向用戶BB轉帳100】

public void testJDBCTransaction() {
    Connection conn = null;
    try {
        // 1.獲取數據庫鏈接
        conn = JDBCUtils.getConnection();
        // 2.開啓事務
        conn.setAutoCommit(false);
        // 3.進行數據庫操做
        String sql1 = "update user_table set balance = balance - 100 where user = ?";
        update(conn, sql1, "AA");

        // 模擬網絡異常
        //System.out.println(10 / 0);

        String sql2 = "update user_table set balance = balance + 100 where user = ?";
        update(conn, sql2, "BB");
        // 4.若沒有異常,則提交事務
        conn.commit();
    } catch (Exception e) {
        e.printStackTrace();
        // 5.如有異常,則回滾事務
        try {
            conn.rollback();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
    } finally {
        try {
            //6.恢復每次DML操做的自動提交功能
            conn.setAutoCommit(true);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        //7.關閉鏈接
        JDBCUtils.closeResource(conn, null, null); 
    }  
}

其中,對數據庫操做的方法爲:

//使用事務之後的通用的增刪改操做(version 2.0)
public void update(Connection conn ,String sql, Object... args) {
    PreparedStatement ps = null;
    try {
        // 1.獲取PreparedStatement的實例 (或:預編譯sql語句)
        ps = conn.prepareStatement(sql);
        // 2.填充佔位符
        for (int i = 0; i < args.length; i++) {
            ps.setObject(i + 1, args[i]);
        }
        // 3.執行sql語句
        ps.execute();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 4.關閉資源
        JDBCUtils.closeResource(null, ps);

    }
}

6.3 事務的ACID屬性

  1. 原子性(Atomicity)
    原子性是指事務是一個不可分割的工做單位,事務中的操做要麼都發生,要麼都不發生。

  2. 一致性(Consistency)
    事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態。

  3. 隔離性(Isolation)
    事務的隔離性是指一個事務的執行不能被其餘事務干擾,即一個事務內部的操做及使用的數據對併發的其餘事務是隔離的,併發執行的各個事務之間不能互相干擾。

  4. 持久性(Durability)
    持久性是指一個事務一旦被提交,它對數據庫中數據的改變就是永久性的,接下來的其餘操做和數據庫故障不該該對其有任何影響。

6.3.1 數據庫的併發問題

  • 對於同時運行的多個事務, 當這些事務訪問數據庫中相同的數據時, 若是沒有采起必要的隔離機制, 就會致使各類併發問題:
    • 髒讀: 對於兩個事務 T1, T2, T1 讀取了已經被 T2 更新但還沒有被提交的字段。以後, 若 T2 回滾, T1讀取的內容就是臨時且無效的。
    • 不可重複讀: 對於兩個事務T1, T2, T1 讀取了一個字段, 而後 T2 更新了該字段。以後, T1再次讀取同一個字段, 值就不一樣了。
    • 幻讀: 對於兩個事務T1, T2, T1 從一個表中讀取了一個字段, 而後 T2 在該表中插入了一些新的行。以後, 若是 T1 再次讀取同一個表, 就會多出幾行。
  • 數據庫事務的隔離性: 數據庫系統必須具備隔離併發運行各個事務的能力, 使它們不會相互影響, 避免各類併發問題。

  • 一個事務與其餘事務隔離的程度稱爲隔離級別。數據庫規定了多種事務隔離級別, 不一樣隔離級別對應不一樣的干擾程度, 隔離級別越高, 數據一致性就越好, 但併發性越弱。

6.3.2 四種隔離級別

  • 數據庫提供的4種事務隔離級別:

  • Oracle 支持的 2 種事務隔離級別:READ COMMITED, SERIALIZABLE。 Oracle 默認的事務隔離級別爲: READ COMMITED

  • Mysql 支持 4 種事務隔離級別。Mysql 默認的事務隔離級別爲: REPEATABLE READ。

6.3.3 在MySql中設置隔離級別

  • 每啓動一個 mysql 程序, 就會得到一個單獨的數據庫鏈接. 每一個數據庫鏈接都有一個全局變量 @@tx_isolation, 表示當前的事務隔離級別。

  • 查看當前的隔離級別:

    SELECT @@tx_isolation;
  • 設置當前 mySQL 鏈接的隔離級別:

    set  transaction isolation level read committed;
  • 設置數據庫系統的全局的隔離級別:

    set global transaction isolation level read committed;
  • 補充操做:

    • 建立mysql數據庫用戶:

      create user tom identified by 'abc123';
    • 授予權限

      #授予經過網絡方式登陸的tom用戶,對全部庫全部表的所有權限,密碼設爲abc123.
      grant all privileges on *.* to tom@'%'  identified by 'abc123'; 
      
       #給tom用戶使用本地命令行方式,授予atguigudb這個庫下的全部表的插刪改查的權限。
      grant select,insert,delete,update on atguigudb.* to tom@localhost identified by 'abc123';

第7章:DAO及相關實現類

  • DAO:Data Access Object訪問數據信息的類和接口,包括了對數據的CRUD(Create、Retrival、Update、Delete),而不包含任何業務相關的信息。有時也稱做:BaseDAO
  • 做用:爲了實現功能的模塊化,更有利於代碼的維護和升級。
  • 下面是尚硅谷JavaWeb階段書城項目中DAO使用的體現:

  • 層次結構:

【BaseDAO.java】

package com.atguigu.bookstore.dao;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;


/**
 * 定義一個用來被繼承的對數據庫進行基本操做的Dao
 * 
 * @author HanYanBing
 *
 * @param <T>
 */
public abstract class BaseDao<T> {
    private QueryRunner queryRunner = new QueryRunner();
    // 定義一個變量來接收泛型的類型
    private Class<T> type;

    // 獲取T的Class對象,獲取泛型的類型,泛型是在被子類繼承時才肯定
    public BaseDao() {
        // 獲取子類的類型
        Class clazz = this.getClass();
        // 獲取父類的類型
        // getGenericSuperclass()用來獲取當前類的父類的類型
        // ParameterizedType表示的是帶泛型的類型
        ParameterizedType parameterizedType = (ParameterizedType) clazz.getGenericSuperclass();
        // 獲取具體的泛型類型 getActualTypeArguments獲取具體的泛型的類型
        // 這個方法會返回一個Type的數組
        Type[] types = parameterizedType.getActualTypeArguments();
        // 獲取具體的泛型的類型·
        this.type = (Class<T>) types[0];
    }

    /**
     * 通用的增刪改操做
     * 
     * @param sql
     * @param params
     * @return
     */
    public int update(Connection conn,String sql, Object... params) {
        int count = 0;
        try {
            count = queryRunner.update(conn, sql, params);
        } catch (SQLException e) {
            e.printStackTrace();
        } 
        return count;
    }

    /**
     * 獲取一個對象
     * 
     * @param sql
     * @param params
     * @return
     */
    public T getBean(Connection conn,String sql, Object... params) {
        T t = null;
        try {
            t = queryRunner.query(conn, sql, new BeanHandler<T>(type), params);
        } catch (SQLException e) {
            e.printStackTrace();
        } 
        return t;
    }

    /**
     * 獲取全部對象
     * 
     * @param sql
     * @param params
     * @return
     */
    public List<T> getBeanList(Connection conn,String sql, Object... params) {
        List<T> list = null;
        try {
            list = queryRunner.query(conn, sql, new BeanListHandler<T>(type), params);
        } catch (SQLException e) {
            e.printStackTrace();
        } 
        return list;
    }

    /**
     * 獲取一個但一值得方法,專門用來執行像 select count(*)...這樣的sql語句
     * 
     * @param sql
     * @param params
     * @return
     */
    public Object getValue(Connection conn,String sql, Object... params) {
        Object count = null;
        try {
            // 調用queryRunner的query方法獲取一個單一的值
            count = queryRunner.query(conn, sql, new ScalarHandler<>(), params);
        } catch (SQLException e) {
            e.printStackTrace();
        } 
        return count;
    }
}

【BookDAO.java】

package com.atguigu.bookstore.dao;

import java.sql.Connection;
import java.util.List;

import com.atguigu.bookstore.beans.Book;
import com.atguigu.bookstore.beans.Page;

public interface BookDao {

    /**
     * 從數據庫中查詢出全部的記錄
     * 
     * @return
     */
    List<Book> getBooks(Connection conn);

    /**
     * 向數據庫中插入一條記錄
     * 
     * @param book
     */
    void saveBook(Connection conn,Book book);

    /**
     * 從數據庫中根據圖書的id刪除一條記錄
     * 
     * @param bookId
     */
    void deleteBookById(Connection conn,String bookId);

    /**
     * 根據圖書的id從數據庫中查詢出一條記錄
     * 
     * @param bookId
     * @return
     */
    Book getBookById(Connection conn,String bookId);

    /**
     * 根據圖書的id從數據庫中更新一條記錄
     * 
     * @param book
     */
    void updateBook(Connection conn,Book book);

    /**
     * 獲取帶分頁的圖書信息
     * 
     * @param page:是隻包含了用戶輸入的pageNo屬性的page對象
     * @return 返回的Page對象是包含了全部屬性的Page對象
     */
    Page<Book> getPageBooks(Connection conn,Page<Book> page);

    /**
     * 獲取帶分頁和價格範圍的圖書信息
     * 
     * @param page:是隻包含了用戶輸入的pageNo屬性的page對象
     * @return 返回的Page對象是包含了全部屬性的Page對象
     */
    Page<Book> getPageBooksByPrice(Connection conn,Page<Book> page, double minPrice, double maxPrice);

}

【UserDAO.java】

package com.atguigu.bookstore.dao;

import java.sql.Connection;

import com.atguigu.bookstore.beans.User;

public interface UserDao {

    /**
     * 根據User對象中的用戶名和密碼從數據庫中獲取一條記錄
     * 
     * @param user
     * @return User 數據庫中有記錄 null 數據庫中無此記錄
     */
    User getUser(Connection conn,User user);

    /**
     * 根據User對象中的用戶名從數據庫中獲取一條記錄
     * 
     * @param user
     * @return true 數據庫中有記錄 false 數據庫中無此記錄
     */
    boolean checkUsername(Connection conn,User user);

    /**
     * 向數據庫中插入User對象
     * 
     * @param user
     */
    void saveUser(Connection conn,User user);
}

【BookDaoImpl.java】

package com.atguigu.bookstore.dao.impl;

import java.sql.Connection;
import java.util.List;

import com.atguigu.bookstore.beans.Book;
import com.atguigu.bookstore.beans.Page;
import com.atguigu.bookstore.dao.BaseDao;
import com.atguigu.bookstore.dao.BookDao;

public class BookDaoImpl extends BaseDao<Book> implements BookDao {

    @Override
    public List<Book> getBooks(Connection conn) {
        // 調用BaseDao中獲得一個List的方法
        List<Book> beanList = null;
        // 寫sql語句
        String sql = "select id,title,author,price,sales,stock,img_path imgPath from books";
        beanList = getBeanList(conn,sql);
        return beanList;
    }

    @Override
    public void saveBook(Connection conn,Book book) {
        // 寫sql語句
        String sql = "insert into books(title,author,price,sales,stock,img_path) values(?,?,?,?,?,?)";
        // 調用BaseDao中通用的增刪改的方法
        update(conn,sql, book.getTitle(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(),book.getImgPath());
    }

    @Override
    public void deleteBookById(Connection conn,String bookId) {
        // 寫sql語句
        String sql = "DELETE FROM books WHERE id = ?";
        // 調用BaseDao中通用增刪改的方法
        update(conn,sql, bookId);
            
    }

    @Override
    public Book getBookById(Connection conn,String bookId) {
        // 調用BaseDao中獲取一個對象的方法
        Book book = null;
        // 寫sql語句
        String sql = "select id,title,author,price,sales,stock,img_path imgPath from books where id = ?";
        book = getBean(conn,sql, bookId);
        return book;
    }

    @Override
    public void updateBook(Connection conn,Book book) {
        // 寫sql語句
        String sql = "update books set title = ? , author = ? , price = ? , sales = ? , stock = ? where id = ?";
        // 調用BaseDao中通用的增刪改的方法
        update(conn,sql, book.getTitle(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getId());
    }

    @Override
    public Page<Book> getPageBooks(Connection conn,Page<Book> page) {
        // 獲取數據庫中圖書的總記錄數
        String sql = "select count(*) from books";
        // 調用BaseDao中獲取一個單一值的方法
        long totalRecord = (long) getValue(conn,sql);
        // 將總記錄數設置都page對象中
        page.setTotalRecord((int) totalRecord);

        // 獲取當前頁中的記錄存放的List
        String sql2 = "select id,title,author,price,sales,stock,img_path imgPath from books limit ?,?";
        // 調用BaseDao中獲取一個集合的方法
        List<Book> beanList = getBeanList(conn,sql2, (page.getPageNo() - 1) * Page.PAGE_SIZE, Page.PAGE_SIZE);
        // 將這個List設置到page對象中
        page.setList(beanList);
        return page;
    }

    @Override
    public Page<Book> getPageBooksByPrice(Connection conn,Page<Book> page, double minPrice, double maxPrice) {
        // 獲取數據庫中圖書的總記錄數
        String sql = "select count(*) from books where price between ? and ?";
        // 調用BaseDao中獲取一個單一值的方法
        long totalRecord = (long) getValue(conn,sql,minPrice,maxPrice);
        // 將總記錄數設置都page對象中
        page.setTotalRecord((int) totalRecord);

        // 獲取當前頁中的記錄存放的List
        String sql2 = "select id,title,author,price,sales,stock,img_path imgPath from books where price between ? and ? limit ?,?";
        // 調用BaseDao中獲取一個集合的方法
        List<Book> beanList = getBeanList(conn,sql2, minPrice , maxPrice , (page.getPageNo() - 1) * Page.PAGE_SIZE, Page.PAGE_SIZE);
        // 將這個List設置到page對象中
        page.setList(beanList);
        
        return page;
    }

}

【UserDaoImpl.java】

package com.atguigu.bookstore.dao.impl;

import java.sql.Connection;

import com.atguigu.bookstore.beans.User;
import com.atguigu.bookstore.dao.BaseDao;
import com.atguigu.bookstore.dao.UserDao;

public class UserDaoImpl extends BaseDao<User> implements UserDao {

    @Override
    public User getUser(Connection conn,User user) {
        // 調用BaseDao中獲取一個對象的方法
        User bean = null;
        // 寫sql語句
        String sql = "select id,username,password,email from users where username = ? and password = ?";
        bean = getBean(conn,sql, user.getUsername(), user.getPassword());
        return bean;
    }

    @Override
    public boolean checkUsername(Connection conn,User user) {
        // 調用BaseDao中獲取一個對象的方法
        User bean = null;
        // 寫sql語句
        String sql = "select id,username,password,email from users where username = ?";
        bean = getBean(conn,sql, user.getUsername());
        return bean != null;
    }

    @Override
    public void saveUser(Connection conn,User user) {
        //寫sql語句
        String sql = "insert into users(username,password,email) values(?,?,?)";
        //調用BaseDao中通用的增刪改的方法
        update(conn,sql, user.getUsername(),user.getPassword(),user.getEmail());
    }

}

【Book.java】

package com.atguigu.bookstore.beans;
/**
 * 圖書類
 * @author songhongkang
 *
 */
public class Book {

    private Integer id;
    private String title; // 書名
    private String author; // 做者
    private double price; // 價格
    private Integer sales; // 銷量
    private Integer stock; // 庫存
    private String imgPath = "static/img/default.jpg"; // 封面圖片的路徑
    //構造器,get(),set(),toString()方法略
}

【Page.java】

package com.atguigu.bookstore.beans;

import java.util.List;
/**
 * 頁碼類
 * @author songhongkang
 *
 */
public class Page<T> {

    private List<T> list; // 每頁查到的記錄存放的集合
    public static final int PAGE_SIZE = 4; // 每頁顯示的記錄數
    private int pageNo; // 當前頁
//  private int totalPageNo; // 總頁數,經過計算獲得
    private int totalRecord; // 總記錄數,經過查詢數據庫獲得

【User.java】

package com.atguigu.bookstore.beans;
/**
 * 用戶類
 * @author songhongkang
 *
 */
public class User {

    private Integer id;
    private String username;
    private String password;
    private String email;

第8章:數據庫鏈接池

8.1 JDBC數據庫鏈接池的必要性

  • 在使用開發基於數據庫的web程序時,傳統的模式基本是按如下步驟:  
    • 在主程序(如servlet、beans)中創建數據庫鏈接
    • 進行sql操做
    • 斷開數據庫鏈接
  • 這種模式開發,存在的問題:
    • 普通的JDBC數據庫鏈接使用 DriverManager 來獲取,每次向數據庫創建鏈接的時候都要將 Connection 加載到內存中,再驗證用戶名和密碼(得花費0.05s~1s的時間)。須要數據庫鏈接的時候,就向數據庫要求一個,執行完成後再斷開鏈接。這樣的方式將會消耗大量的資源和時間。數據庫的鏈接資源並無獲得很好的重複利用。若同時有幾百人甚至幾千人在線,頻繁的進行數據庫鏈接操做將佔用不少的系統資源,嚴重的甚至會形成服務器的崩潰。
    • 對於每一次數據庫鏈接,使用完後都得斷開。不然,若是程序出現異常而未能關閉,將會致使數據庫系統中的內存泄漏,最終將致使重啓數據庫。(回憶:何爲Java的內存泄漏?)
    • 這種開發不能控制被建立的鏈接對象數,系統資源會被毫無顧及的分配出去,如鏈接過多,也可能致使內存泄漏,服務器崩潰。

8.2 數據庫鏈接池技術

  • 爲解決傳統開發中的數據庫鏈接問題,能夠採用數據庫鏈接池技術。
  • 數據庫鏈接池的基本思想:就是爲數據庫鏈接創建一個「緩衝池」。預先在緩衝池中放入必定數量的鏈接,當須要創建數據庫鏈接時,只需從「緩衝池」中取出一個,使用完畢以後再放回去。

  • 數據庫鏈接池負責分配、管理和釋放數據庫鏈接,它容許應用程序重複使用一個現有的數據庫鏈接,而不是從新創建一個
  • 數據庫鏈接池在初始化時將建立必定數量的數據庫鏈接放到鏈接池中,這些數據庫鏈接的數量是由最小數據庫鏈接數來設定的。不管這些數據庫鏈接是否被使用,鏈接池都將一直保證至少擁有這麼多的鏈接數量。鏈接池的最大數據庫鏈接數量限定了這個鏈接池能佔有的最大鏈接數,當應用程序向鏈接池請求的鏈接數超過最大鏈接數量時,這些請求將被加入到等待隊列中。

  • 工做原理:

  • 數據庫鏈接池技術的優勢

    1. 資源重用

    因爲數據庫鏈接得以重用,避免了頻繁建立,釋放鏈接引發的大量性能開銷。在減小系統消耗的基礎上,另外一方面也增長了系統運行環境的平穩性。

    2. 更快的系統反應速度

    數據庫鏈接池在初始化過程當中,每每已經建立了若干數據庫鏈接置於鏈接池中備用。此時鏈接的初始化工做均已完成。對於業務請求處理而言,直接利用現有可用鏈接,避免了數據庫鏈接初始化和釋放過程的時間開銷,從而減小了系統的響應時間

    3. 新的資源分配手段

    對於多應用共享同一數據庫的系統而言,可在應用層經過數據庫鏈接池的配置,實現某一應用最大可用數據庫鏈接數的限制,避免某一應用獨佔全部的數據庫資源

    4. 統一的鏈接管理,避免數據庫鏈接泄漏

    在較爲完善的數據庫鏈接池實現中,可根據預先的佔用超時設定,強制回收被佔用鏈接,從而避免了常規數據庫鏈接操做中可能出現的資源泄露

8.3 多種開源的數據庫鏈接池

  • JDBC 的數據庫鏈接池使用 javax.sql.DataSource 來表示,DataSource 只是一個接口,該接口一般由服務器(Weblogic, WebSphere, Tomcat)提供實現,也有一些開源組織提供實現:
    • DBCP 是Apache提供的數據庫鏈接池。tomcat 服務器自帶dbcp數據庫鏈接池。速度相對c3p0較快,但因自身存在BUG,Hibernate3已再也不提供支持。
    • C3P0 是一個開源組織提供的一個數據庫鏈接池,速度相對較慢,穩定性還能夠。hibernate官方推薦使用
    • Proxool 是sourceforge下的一個開源項目數據庫鏈接池,有監控鏈接池狀態的功能,穩定性較c3p0差一點
    • BoneCP 是一個開源組織提供的數據庫鏈接池,速度快
    • Druid 是阿里提供的數據庫鏈接池,聽說是集DBCP 、C3P0 、Proxool 優勢於一身的數據庫鏈接池,可是速度不肯定是否有BoneCP快
  • DataSource 一般被稱爲數據源,它包含鏈接池和鏈接池管理兩個部分,習慣上也常常把 DataSource 稱爲鏈接池
  • DataSource用來取代DriverManager來獲取Connection,獲取速度快,同時能夠大幅度提升數據庫訪問速度。
  • 特別注意:
    • 數據源和數據庫鏈接不一樣,數據源無需建立多個,它是產生數據庫鏈接的工廠,所以整個應用只須要一個數據源便可。
    • 當數據庫訪問結束後,程序仍是像之前同樣關閉數據庫鏈接:conn.close(); 但conn.close()並無關閉數據庫的物理鏈接,它僅僅把數據庫鏈接釋放,歸還給了數據庫鏈接池。

8.3.1 C3P0數據庫鏈接池

  • 獲取鏈接方式一
//使用C3P0數據庫鏈接池的方式,獲取數據庫的鏈接:不推薦
public static Connection getConnection1() throws Exception{
    ComboPooledDataSource cpds = new ComboPooledDataSource();
    cpds.setDriverClass("com.mysql.jdbc.Driver"); 
    cpds.setJdbcUrl("jdbc:mysql://localhost:3306/test");
    cpds.setUser("root");
    cpds.setPassword("abc123");
        
//  cpds.setMaxPoolSize(100);
    
    Connection conn = cpds.getConnection();
    return conn;
}
  • 獲取鏈接方式二
//使用C3P0數據庫鏈接池的配置文件方式,獲取數據庫的鏈接:推薦
private static DataSource cpds = new ComboPooledDataSource("helloc3p0");
public static Connection getConnection2() throws SQLException{
    Connection conn = cpds.getConnection();
    return conn;
}

其中,src下的配置文件爲:【c3p0-config.xml】

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
    <named-config name="helloc3p0">
        <!-- 獲取鏈接的4個基本信息 -->
        <property name="user">root</property>
        <property name="password">abc123</property>
        <property name="jdbcUrl">jdbc:mysql:///test</property>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        
        <!-- 涉及到數據庫鏈接池的管理的相關屬性的設置 -->
        <!-- 若數據庫中鏈接數不足時, 一次向數據庫服務器申請多少個鏈接 -->
        <property name="acquireIncrement">5</property>
        <!-- 初始化數據庫鏈接池時鏈接的數量 -->
        <property name="initialPoolSize">5</property>
        <!-- 數據庫鏈接池中的最小的數據庫鏈接數 -->
        <property name="minPoolSize">5</property>
        <!-- 數據庫鏈接池中的最大的數據庫鏈接數 -->
        <property name="maxPoolSize">10</property>
        <!-- C3P0 數據庫鏈接池能夠維護的 Statement 的個數 -->
        <property name="maxStatements">20</property>
        <!-- 每一個鏈接同時可使用的 Statement 對象的個數 -->
        <property name="maxStatementsPerConnection">5</property>

    </named-config>
</c3p0-config>

8.3.2 DBCP數據庫鏈接池

  • DBCP 是 Apache 軟件基金組織下的開源鏈接池實現,該鏈接池依賴該組織下的另外一個開源系統:Common-pool。如需使用該鏈接池實現,應在系統中增長以下兩個 jar 文件:
    • Commons-dbcp.jar:鏈接池的實現
    • Commons-pool.jar:鏈接池實現的依賴庫
  • Tomcat 的鏈接池正是採用該鏈接池來實現的。該數據庫鏈接池既能夠與應用服務器整合使用,也可由應用程序獨立使用。
  • 數據源和數據庫鏈接不一樣,數據源無需建立多個,它是產生數據庫鏈接的工廠,所以整個應用只須要一個數據源便可。
  • 當數據庫訪問結束後,程序仍是像之前同樣關閉數據庫鏈接:conn.close(); 但上面的代碼並無關閉數據庫的物理鏈接,它僅僅把數據庫鏈接釋放,歸還給了數據庫鏈接池。
  • 配置屬性說明
屬性 默認值 說明
initialSize 0 鏈接池啓動時建立的初始化鏈接數量
maxActive 8 鏈接池中可同時鏈接的最大的鏈接數
maxIdle 8 鏈接池中最大的空閒的鏈接數,超過的空閒鏈接將被釋放,若是設置爲負數表示不限制
minIdle 0 鏈接池中最小的空閒的鏈接數,低於這個數量會被建立新的鏈接。該參數越接近maxIdle,性能越好,由於鏈接的建立和銷燬,都是須要消耗資源的;可是不能太大。
maxWait 無限制 最大等待時間,當沒有可用鏈接時,鏈接池等待鏈接釋放的最大時間,超過該時間限制會拋出異常,若是設置-1表示無限等待
poolPreparedStatements false 開啓池的Statement是否prepared
maxOpenPreparedStatements 無限制 開啓池的prepared 後的同時最大鏈接數
minEvictableIdleTimeMillis 鏈接池中鏈接,在時間段內一直空閒, 被逐出鏈接池的時間
removeAbandonedTimeout 300 超過期間限制,回收沒有用(廢棄)的鏈接
removeAbandoned false 超過removeAbandonedTimeout時間後,是否進 行沒用鏈接(廢棄)的回收
  • 獲取鏈接方式一:
public static Connection getConnection3() throws Exception {
    BasicDataSource source = new BasicDataSource();
        
    source.setDriverClassName("com.mysql.jdbc.Driver");
    source.setUrl("jdbc:mysql:///test");
    source.setUsername("root");
    source.setPassword("abc123");
        
    //
    source.setInitialSize(10);
        
    Connection conn = source.getConnection();
    return conn;
}
  • 獲取鏈接方式二:
//使用dbcp數據庫鏈接池的配置文件方式,獲取數據庫的鏈接:推薦
private static DataSource source = null;
static{
    try {
        Properties pros = new Properties();
        
        InputStream is = DBCPTest.class.getClassLoader().getResourceAsStream("dbcp.properties");
            
        pros.load(is);
        //根據提供的BasicDataSourceFactory建立對應的DataSource對象
        source = BasicDataSourceFactory.createDataSource(pros);
    } catch (Exception e) {
        e.printStackTrace();
    }
        
}
public static Connection getConnection4() throws Exception {
        
    Connection conn = source.getConnection();
    
    return conn;
}

其中,src下的配置文件爲:【dbcp.properties】

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useServerPrepStmts=false
username=root
password=abc123

initialSize=10
#...

8.3.3 Druid(德魯伊)數據庫鏈接池

Druid是阿里巴巴開源平臺上一個數據庫鏈接池實現,它結合了C3P0、DBCP、Proxool等DB池的優勢,同時加入了日誌監控,能夠很好的監控DB池鏈接和SQL的執行狀況,能夠說是針對監控而生的DB鏈接池,能夠說是目前最好的鏈接池之一。

package com.atguigu.druid;

import java.sql.Connection;
import java.util.Properties;

import javax.sql.DataSource;

import com.alibaba.druid.pool.DruidDataSourceFactory;

public class TestDruid {
    public static void main(String[] args) throws Exception {
        Properties pro = new Properties();
         pro.load(TestDruid.class.getClassLoader().getResourceAsStream("druid.properties"));
        DataSource ds = DruidDataSourceFactory.createDataSource(pro);
        Connection conn = ds.getConnection();
        System.out.println(conn);
    }
}

其中,src下的配置文件爲:【druid.properties】

url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
username=root
password=123456
driverClassName=com.mysql.jdbc.Driver

initialSize=10
maxActive=20
maxWait=1000
filters=wall
  • 詳細配置參數:
配置 缺省 說明
name 配置這個屬性的意義在於,若是存在多個數據源,監控的時候能夠經過名字來區分開來。 若是沒有配置,將會生成一個名字,格式是:」DataSource-」 + System.identityHashCode(this)
url 鏈接數據庫的url,不一樣數據庫不同。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 鏈接數據庫的用戶名
password 鏈接數據庫的密碼。若是你不但願密碼直接寫在配置文件中,可使用ConfigFilter。詳細看這裏:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter
driverClassName 根據url自動識別 這一項可配可不配,若是不配置druid會根據url自動識別dbType,而後選擇相應的driverClassName(建議配置下)
initialSize 0 初始化時創建物理鏈接的個數。初始化發生在顯示調用init方法,或者第一次getConnection時
maxActive 8 最大鏈接池數量
maxIdle 8 已經再也不使用,配置了也沒效果
minIdle 最小鏈接池數量
maxWait 獲取鏈接時最大等待時間,單位毫秒。配置了maxWait以後,缺省啓用公平鎖,併發效率會有所降低,若是須要能夠經過配置useUnfairLock屬性爲true使用非公平鎖。
poolPreparedStatements false 是否緩存preparedStatement,也就是PSCache。PSCache對支持遊標的數據庫性能提高巨大,好比說oracle。在mysql下建議關閉。
maxOpenPreparedStatements -1 要啓用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改成true。在Druid中,不會存在Oracle下PSCache佔用內存過多的問題,能夠把這個數值配置大一些,好比說100
validationQuery 用來檢測鏈接是否有效的sql,要求是一個查詢語句。若是validationQuery爲null,testOnBorrow、testOnReturn、testWhileIdle都不會其做用。
testOnBorrow true 申請鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能。
testOnReturn false 歸還鏈接時執行validationQuery檢測鏈接是否有效,作了這個配置會下降性能
testWhileIdle false 建議配置爲true,不影響性能,而且保證安全性。申請鏈接的時候檢測,若是空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測鏈接是否有效。
timeBetweenEvictionRunsMillis 有兩個含義: 1)Destroy線程會檢測鏈接的間隔時間2)testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明
numTestsPerEvictionRun 再也不使用,一個DruidDataSource只支持一個EvictionRun
minEvictableIdleTimeMillis
connectionInitSqls 物理鏈接初始化的時候執行的sql
exceptionSorter 根據dbType自動識別 當數據庫拋出一些不可恢復的異常時,拋棄鏈接
filters 屬性類型是字符串,經過別名的方式配置擴展插件,經常使用的插件有: 監控統計用的filter:stat日誌用的filter:log4j防護sql注入的filter:wall
proxyFilters 類型是List,若是同時配置了filters和proxyFilters,是組合關係,並不是替換關係

第9章:Apache-DBUtils實現CRUD操做

9.1 Apache-DBUtils簡介

  • commons-dbutils 是 Apache 組織提供的一個開源 JDBC工具類庫,它是對JDBC的簡單封裝,學習成本極低,而且使用dbutils能極大簡化jdbc編碼的工做量,同時也不會影響程序的性能。

  • API介紹:
    • org.apache.commons.dbutils.QueryRunner
    • org.apache.commons.dbutils.ResultSetHandler
    • 工具類:org.apache.commons.dbutils.DbUtils
  • API包說明:

9.2 主要API的使用

9.2.1 DbUtils

  • DbUtils :提供如關閉鏈接、裝載JDBC驅動程序等常規工做的工具類,裏面的全部方法都是靜態的。主要方法以下:
    • public static void close(…) throws java.sql.SQLException: DbUtils類提供了三個重載的關閉方法。這些方法檢查所提供的參數是否是NULL,若是不是的話,它們就關閉Connection、Statement和ResultSet。
    • public static void closeQuietly(…): 這一類方法不只能在Connection、Statement和ResultSet爲NULL狀況下避免關閉,還能隱藏一些在程序中拋出的SQLEeception。
    • public static void commitAndClose(Connection conn)throws SQLException: 用來提交鏈接的事務,而後關閉鏈接
    • public static void commitAndCloseQuietly(Connection conn): 用來提交鏈接,而後關閉鏈接,而且在關閉鏈接時不拋出SQL異常。
    • public static void rollback(Connection conn)throws SQLException:容許conn爲null,由於方法內部作了判斷
    • public static void rollbackAndClose(Connection conn)throws SQLException
    • rollbackAndCloseQuietly(Connection)
    • public static boolean loadDriver(java.lang.String driverClassName):這一方裝載並註冊JDBC驅動程序,若是成功就返回true。使用該方法,你不須要捕捉這個異常ClassNotFoundException。

9.2.2 QueryRunner類

  • 該類簡單化了SQL查詢,它與ResultSetHandler組合在一塊兒使用能夠完成大部分的數據庫操做,可以大大減小編碼量。

  • QueryRunner類提供了兩個構造器:
    • 默認的構造器
    • 須要一個 javax.sql.DataSource 來做參數的構造器
  • QueryRunner類的主要方法:
    • 更新
      • public int update(Connection conn, String sql, Object... params) throws SQLException:用來執行一個更新(插入、更新或刪除)操做。
      • ......
    • 插入
      • public T insert(Connection conn,String sql,ResultSetHandler rsh, Object... params) throws SQLException:只支持INSERT語句,其中 rsh - The handler used to create the result object from the ResultSet of auto-generated keys. 返回值: An object generated by the handler.即自動生成的鍵值
      • ....
    • 批處理
      • public int[] batch(Connection conn,String sql,Object[][] params)throws SQLException: INSERT, UPDATE, or DELETE語句
      • public T insertBatch(Connection conn,String sql,ResultSetHandler rsh,Object[][] params)throws SQLException:只支持INSERT語句
      • .....
    • 查詢
      • public Object query(Connection conn, String sql, ResultSetHandler rsh,Object... params) throws SQLException:執行一個查詢操做,在這個查詢中,對象數組中的每一個元素值被用來做爲查詢語句的置換參數。該方法會自行處理 PreparedStatement 和 ResultSet 的建立和關閉。
      • ......
  • 測試

// 測試添加
@Test
public void testInsert() throws Exception {
    QueryRunner runner = new QueryRunner();
    Connection conn = JDBCUtils.getConnection3();
    String sql = "insert into customers(name,email,birth)values(?,?,?)";
    int count = runner.update(conn, sql, "何成飛", "he@qq.com", "1992-09-08");

    System.out.println("添加了" + count + "條記錄");
        
    JDBCUtils.closeResource(conn, null);

}
// 測試刪除
@Test
public void testDelete() throws Exception {
    QueryRunner runner = new QueryRunner();
    Connection conn = JDBCUtils.getConnection3();
    String sql = "delete from customers where id < ?";
    int count = runner.update(conn, sql,3);

    System.out.println("刪除了" + count + "條記錄");
        
    JDBCUtils.closeResource(conn, null);

}

9.2.3 ResultSetHandler接口及實現類

  • 該接口用於處理 java.sql.ResultSet,將數據按要求轉換爲另外一種形式。

  • ResultSetHandler 接口提供了一個單獨的方法:Object handle (java.sql.ResultSet .rs)。

  • 接口的主要實現類:

    • ArrayHandler:把結果集中的第一行數據轉成對象數組。
    • ArrayListHandler:把結果集中的每一行數據都轉成一個數組,再存放到List中。
    • BeanHandler:將結果集中的第一行數據封裝到一個對應的JavaBean實例中。
    • BeanListHandler:將結果集中的每一行數據都封裝到一個對應的JavaBean實例中,存放到List裏。
    • ColumnListHandler:將結果集中某一列的數據存放到List中。
    • KeyedHandler(name):將結果集中的每一行數據都封裝到一個Map裏,再把這些map再存到一個map裏,其key爲指定的key。
    • MapHandler:將結果集中的第一行數據封裝到一個Map裏,key是列名,value就是對應的值。
    • MapListHandler:將結果集中的每一行數據都封裝到一個Map裏,而後再存放到List
    • ScalarHandler:查詢單個值對象
  • 測試

/*
 * 測試查詢:查詢一條記錄
 * 
 * 使用ResultSetHandler的實現類:BeanHandler
 */
@Test
public void testQueryInstance() throws Exception{
    QueryRunner runner = new QueryRunner();

    Connection conn = JDBCUtils.getConnection3();
        
    String sql = "select id,name,email,birth from customers where id = ?";
        
    //
    BeanHandler<Customer> handler = new BeanHandler<>(Customer.class);
    Customer customer = runner.query(conn, sql, handler, 23);
    System.out.println(customer);   
    JDBCUtils.closeResource(conn, null);
}
/*
 * 測試查詢:查詢多條記錄構成的集合
 * 
 * 使用ResultSetHandler的實現類:BeanListHandler
 */
@Test
public void testQueryList() throws Exception{
    QueryRunner runner = new QueryRunner();

    Connection conn = JDBCUtils.getConnection3();
        
    String sql = "select id,name,email,birth from customers where id < ?";
        
    //
    BeanListHandler<Customer> handler = new BeanListHandler<>(Customer.class);
    List<Customer> list = runner.query(conn, sql, handler, 23);
    list.forEach(System.out::println);
        
    JDBCUtils.closeResource(conn, null);
}
/*
 * 自定義ResultSetHandler的實現類
 */
@Test
public void testQueryInstance1() throws Exception{
    QueryRunner runner = new QueryRunner();

    Connection conn = JDBCUtils.getConnection3();
        
    String sql = "select id,name,email,birth from customers where id = ?";
        
    ResultSetHandler<Customer> handler = new ResultSetHandler<Customer>() {

        @Override
        public Customer handle(ResultSet rs) throws SQLException {
            System.out.println("handle");
//          return new Customer(1,"Tom","tom@126.com",new Date(123323432L));
                
            if(rs.next()){
                int id = rs.getInt("id");
                String name = rs.getString("name");
                String email = rs.getString("email");
                Date birth = rs.getDate("birth");
                    
                return new Customer(id, name, email, birth);
            }
            return null;
                
        }
    };
        
    Customer customer = runner.query(conn, sql, handler, 23);
        
    System.out.println(customer);
        
    JDBCUtils.closeResource(conn, null);
}
/*
 * 如何查詢相似於最大的,最小的,平均的,總和,個數相關的數據,
 * 使用ScalarHandler
 * 
 */
@Test
public void testQueryValue() throws Exception{
    QueryRunner runner = new QueryRunner();

    Connection conn = JDBCUtils.getConnection3();
        
    //測試一:
//  String sql = "select count(*) from customers where id < ?";
//  ScalarHandler handler = new ScalarHandler();
//  long count = (long) runner.query(conn, sql, handler, 20);
//  System.out.println(count);
        
    //測試二:
    String sql = "select max(birth) from customers";
    ScalarHandler handler = new ScalarHandler();
    Date birth = (Date) runner.query(conn, sql, handler);
    System.out.println(birth);
        
    JDBCUtils.closeResource(conn, null);
}

JDBC總結

總結
@Test
public void testUpdateWithTx() {
        
    Connection conn = null;
    try {
        //1.獲取鏈接的操做(
        //① 手寫的鏈接:JDBCUtils.getConnection();
        //② 使用數據庫鏈接池:C3P0;DBCP;Druid
        //2.對數據表進行一系列CRUD操做
        //① 使用PreparedStatement實現通用的增刪改、查詢操做(version 1.0 \ version 2.0)
//version2.0的增刪改public void update(Connection conn,String sql,Object ... args){}
//version2.0的查詢 public <T> T getInstance(Connection conn,Class<T> clazz,String sql,Object ... args){}
        //② 使用dbutils提供的jar包中提供的QueryRunner類
            
        //提交數據
        conn.commit();
            
    
    } catch (Exception e) {
        e.printStackTrace();
            
            
        try {
            //回滾數據
            conn.rollback();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
            
    }finally{
        //3.關閉鏈接等操做
        //① JDBCUtils.closeResource();
        //② 使用dbutils提供的jar包中提供的DbUtils類提供了關閉的相關操做
            
    }
}
相關文章
相關標籤/搜索