天天看點

JDBC-

文章目錄

    • JDBC
    • 1,JDBC概述
      • 1.1 JDBC概念
      • 1.2 JDBC本質
      • 1.3 JDBC好處
    • 2,JDBC快速入門
      • 2.1 編寫代碼步驟
      • 2.2 具體操作
    • 3,JDBC API詳解
      • 3.1 DriverManager
      • 3.2 Connection (事務歸我管)
        • 3.2.1 擷取執行對象
        • 3.2.2 事務管理
      • 3.3 Statement
        • 3.3.1 概述
        • 3.3.2 代碼實作
      • 3.4 ResultSet
        • 3.4.1 概述
        • 3.4.2 代碼實作
      • 3.5 案例
      • 3.6 PreparedStatement
        • 3.6.1 SQL注入
        • 3.6.2 代碼模拟SQL注入問題
        • 3.6.3 PreparedStatement概述
        • 3.6.4 使用PreparedStatement改進
        • 3.6.5 PreparedStatement原理
    • 4,資料庫連接配接池

JDBC

今日目标

  • 掌握JDBC的的CRUD
  • 了解JDBC中各個對象的作用
  • 掌握Druid的使用

1,JDBC概述

在開發中我們使用的是java語言,那麼勢必要通過java語言操作資料庫中的資料。這就是接下來要學習的JDBC。

1.1 JDBC概念

JDBC 就是使用Java語言操作關系型資料庫的一套API

全稱:( Java DataBase Connectivity ) Java 資料庫連接配接

JDBC-

我們開發的同一套Java代碼是無法操作不同的關系型資料庫,因為每一個關系型資料庫的底層實作細節都不一樣。如果這樣,問題就很大了,在公司中可以在開發階段使用的是MySQL資料庫,而上線時公司最終選用oracle資料庫,我們就需要對代碼進行大批量修改,這顯然并不是我們想看到的。我們要做到的是同一套Java代碼操作不同的關系型資料庫,而此時sun公司就指定了一套标準接口(JDBC),JDBC中定義了所有操作關系型資料庫的規則。衆所周知接口是無法直接使用的,我們需要使用接口的實作類,而這套實作類(稱之為:驅動)就由各自的資料庫廠商給出。

1.2 JDBC本質

  • 官方(sun公司)定義的一套操作所有關系型資料庫的規則,即接口
  • 各個資料庫廠商去實作這套接口,提供資料庫驅動jar包
  • 我們可以使用這套接口(JDBC)程式設計,真正執行的代碼是驅動jar包中的實作類

1.3 JDBC好處

  • 各資料庫廠商使用相同的接口,Java代碼不需要針對不同資料庫分别開發
  • 可随時替換底層資料庫,通路資料庫的Java代碼基本不變

以後編寫操作資料庫的代碼隻需要面向JDBC(接口),操作哪兒個關系型資料庫就需要導入該資料庫的驅動包,如需要操作MySQL資料庫,就需要再項目中導入MySQL資料庫的驅動包。如下圖就是MySQL驅動包

所謂驅動,就是各廠商對jdbc這套接口的實作類

JDBC-

2,JDBC快速入門

先來看看通過Java操作資料庫的流程

JDBC-

第一步:編寫Java代碼

第二步:Java代碼将SQL發送到MySQL服務端

第三步:MySQL服務端接收到SQL語句并執行該SQL語句

第四步:将SQL語句執行的結果傳回給Java代碼

2.1 編寫代碼步驟

  • 建立工程,導入驅動jar包
JDBC-
  • 注冊驅動

    擷取位元組碼,其實還有一個天然的作用,就是将類加載進記憶體

  • 擷取連接配接

    Java代碼需要發送SQL給MySQL服務端,就需要先建立連接配接

  • 定義SQL語句
  • 擷取執行SQL對象

    執行SQL語句需要SQL執行對象,而這個執行對象就是Statement對象

  • 執行SQL
  • 處理傳回結果
  • 釋放資源

2.2 具體操作

  • 建立新的空的項目。定義項目的名稱,并指定位置
JDBC-
  • 對項目進行設定,JDK版本、編譯版本
JDBC-

可以裝多個jdk,然後選擇哪個即可

  • 建立子產品,指定子產品的名稱及位置
JDBC-
  • 導入驅動包

    将mysql的驅動包放在子產品下的lib目錄(随意命名)下,并将該jar包添加為庫檔案

    lib目錄和src同級别 (不然也建立不了目錄,隻能建立包)

JDBC-
  • 在添加為庫檔案的時候,有如下三個選項
    • Global Library : 全局有效
    • Project Library : 項目有效
    • Module Library : 子產品有效
JDBC-

改用全局有效試試

  • 在src下建立類
    JDBC-
  • 編寫代碼如下
/**
 * JDBC快速入門
 *
 * 注意 驅動包 我導入成全局的了
 */
public class JDBCDemo {
    public static void main(String[] args) throws Exception {
        //1. 注冊驅動
        //Class.forName("com.mysql.jdbc.Driver");//将類加載到記憶體中
        //mysql5之後 可以省略注冊這一步了 自動注冊

        //2. 擷取連接配接
        String url = "jdbc:mysql://127.0.0.1:3306/db1";
        String user = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, user, password);

        //3. 定義sql
        String sql = "update account set money = 2000 where id = 1";

        //4. 擷取執行sql的對象 Statement
        Statement st = conn.createStatement();

        //5. 執行sql 修改語句對應的方法
        int n = st.executeUpdate(sql); //傳回受影響的行數

        //6. 處理結果 (此處就列印下行數了)
        System.out.println(n);

        //7. 釋放資源 (先開後放)
        st.close();
        conn.close();

    }
}
           

3,JDBC API詳解

3.1 DriverManager

DriverManager(驅動管理類)作用:

  • 注冊驅動
    JDBC-

    registerDriver方法是用于注冊驅動的,但是我們之前做的入門案例并不是這樣寫的。而是如下實作

    我們查詢MySQL提供的Driver類,看它是如何實作的,源碼如下:

    JDBC-

    加載Driver類的時候靜态代碼塊會執行,也就會registerDriver()注冊驅動程式了

    在該類中的靜态代碼塊中已經執行了

    DriverManager

    對象的

    registerDriver()

    方法進行驅動的注冊了,那麼我們隻需要加載

    Driver

    類,該靜态代碼塊就會執行。而

    Class.forName("com.mysql.jdbc.Driver");

    就可以加載

    Driver

    類。
    提示:
    • MySQL 5之後的驅動包,可以省略注冊驅動的步驟
    • 自動加載jar包中META-INF/services/java.sql.Driver檔案中的驅動類
JDBC-
  • 擷取資料庫連接配接

    [外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-6NtgYq0r-1677226449633)(assets/image-20210725171355278.png)]

    參數說明:

    • url : 連接配接路徑

      文法:jdbc:mysql://ip位址(域名):端口号/資料庫名稱?參數鍵值對1&參數鍵值對2…

      示例:jdbc:mysql://127.0.0.1:3306/db1

      細節:

      • 如果連接配接的是本機mysql伺服器,并且mysql服務預設端口是3306,則url可以簡寫為:jdbc:mysql:///資料庫名稱?參數鍵值對
      • 配置 useSSL=false 參數,禁用安全連接配接方式,解決警告提示
    • user :使用者名
    • poassword :密碼

代碼簡化1:

/**
 * JDBC API詳解:DriverManger
 *
 */
public class JDBCDemo2_DriverManager {
    public static void main(String[] args) throws Exception {
        //1. 注冊驅動
        //Class.forName("com.mysql.jdbc.Driver");//将類加載到記憶體中 //Driver類的靜态代碼塊注冊的
        //mysql5之後 可以省略注冊這一步了 自動注冊

        //2. 擷取連接配接
        // 1) 如果連接配接的是本機的mysql 并且端口是預設的3306 可以簡化書寫
        // 2) 配置 useSSL=false 參數,禁用安全連接配接方式,解決警告提示 (輸出就沒有警告了)
        String url = "jdbc:mysql:///db1?useSSL=false";
        String user = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, user, password);

        //3. 定義sql
        String sql = "update account set money = 2000 where id = 1";

        //4. 擷取執行sql的對象 Statement
        Statement st = conn.createStatement();

        //5. 執行sql 修改語句對應的方法
        int n = st.executeUpdate(sql); //傳回受影響的行數

        //6. 處理結果 (此處就列印下行數了)
        System.out.println(n);

        //7. 釋放資源 (先開後放)
        st.close();
        conn.close();

    }
}
           

3.2 Connection (事務歸我管)

Connection(資料庫連接配接對象)作用:

  • 擷取執行 SQL 的對象
  • 管理事務

3.2.1 擷取執行對象

  • 普通執行SQL對象

    入門案例中就是通過該方法擷取的執行對象。

  • 預編譯SQL的執行SQL對象:防止SQL注入

    通過這種方式擷取的

    PreparedStatement

    SQL語句執行對象是我們一會重點要進行講解的,它可以防止SQL注入。
  • 執行存儲過程的對象

    通過這種方式擷取的

    CallableStatement

    執行對象是用來執行存儲過程的,而存儲過程在MySQL中不常用,是以這個我們将不進行講解。

3.2.2 事務管理

先回顧一下MySQL事務管理的操作:

  • 開啟事務 : BEGIN; 或者 START TRANSACTION;
  • 送出事務 : COMMIT;
  • 復原事務 : ROLLBACK;
MySQL預設是自動送出事務

接下來學習JDBC事務管理的方法。

Connection幾口中定義了3個對應的方法:

  • 開啟事務
    JDBC-
    參與autoCommit 表示是否自動送出事務,true表示自動送出事務,false表示手動送出事務。而開啟事務需要将該參數設為為false。
  • 送出事務
    JDBC-
  • 復原事務
    JDBC-

具體代碼實作如下:

public class JDBCDemo3_Connection {
    public static void main(String[] args) throws Exception {
        //1. 注冊驅動
        //Class.forName("com.mysql.jdbc.Driver");//将類加載到記憶體中 //Driver類的靜态代碼塊注冊的
        //mysql5之後 可以省略注冊這一步了 自動注冊

        //2. 擷取連接配接
        // 1) 如果連接配接的是本機的mysql 并且端口是預設的3306 可以簡化書寫
        // 2) 配置 useSSL=false 參數,禁用安全連接配接方式,解決警告提示 (輸出就沒有警告了)
        String url = "jdbc:mysql:///db1?useSSL=false";
        String user = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, user, password);

        //3. 定義sql
        String sql1 = "update account set money = money + 500 where id = 1";
        String sql2 = "update account set money = money - 500 where id = 2";

        //4. 擷取執行sql的對象 Statement
        Statement st = conn.createStatement();

        try {
            //S1: 執行sql之前開啟事務
            conn.setAutoCommit(false);
            //5. 執行sql 修改語句對應的方法
            int n1 = st.executeUpdate(sql1); //傳回受影響的行數
            System.out.println(n1);//6.處理結果
            int i = 3/0; //自己造異常 模拟
            int n2 = st.executeUpdate(sql2); //傳回受影響的行數
            System.out.println(n2);//6.處理結果
            //S2: 結果處理完,說明執行成功,送出事務
            conn.commit();
        }catch (Exception e){
            //S2: 一旦發生異常就復原
            conn.rollback();
            e.printStackTrace();
        }finally {
            //7. 釋放資源 (先開後放)  不管有沒有異常
            st.close();
            conn.close();
        }
    }
}
           

3.3 Statement

3.3.1 概述

Statement對象的作用就是用來執行SQL語句。而針對不同類型的SQL語句使用的方法也不一樣。

  • 執行DDL、DML語句
    JDBC-
  • 執行DQL語句
    JDBC-
    該方法涉及到了

    ResultSet

    對象,而這個對象我們還沒有學習,一會再重點講解。
JDBC-

3.3.2 代碼實作

  • 執行DML語句
@Test
    public void testDML() throws SQLException {
        //1. 注冊驅動 省略

        //2. 擷取連接配接
        String url = "jdbc:mysql:///db1?useSSL=false";
        String user = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, user, password);

        //3. 定義sql
        String sql = "update account set money = 3000 where id = 1";

        //4. 擷取執行sql的對象 Statement
        Statement st = conn.createStatement();

        //5. 執行sql 修改語句對應的方法
        int n = st.executeUpdate(sql); //執行DML語句後,受影響的行數

        //6. 處理結果 (此處就列印下行數了)
        if(n>0){
            System.out.println("修改成功!");
        }else {
            System.out.println("失敗~");
        }

        //7. 釋放資源 (先開後放)
        st.close();
        conn.close();
    }
           
  • 執行DDL語句
@Test
    public void testDDL() throws SQLException {
        //1. 注冊驅動 省略

        //2. 擷取連接配接
        String url = "jdbc:mysql:///db1?useSSL=false";
        String user = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, user, password);

        //3. 定義sql
        String sql = "create database db2";

        //4. 擷取執行sql的對象 Statement
        Statement st = conn.createStatement();

        //5. 執行sql 修改語句對應的方法
        int n = st.executeUpdate(sql); //執行DML語句後,受影響的行數

        //6. 處理結果 (此處就列印下行數了)
        if(n>0){
            System.out.println("建立成功!");
        }else {
            System.out.println("失敗~");
        }

        //7. 釋放資源 (先開後放)
        st.close();
        conn.close();
    }
    @Test
    public void testDDL2() throws SQLException {
        //1. 注冊驅動 省略

        //2. 擷取連接配接
        String url = "jdbc:mysql:///db1?useSSL=false";
        String user = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, user, password);

        //3. 定義sql
        String sql = "drop database if exists db2";

        //4. 擷取執行sql的對象 Statement
        Statement st = conn.createStatement();

        //5. 執行sql 修改語句對應的方法
        int n = st.executeUpdate(sql); //執行DML語句後,受影響的行數

        //6. 處理結果 (此處就列印下行數了)
       System.out.println(n);//DDL 删除操作 執行成功 也傳回0 不一定傳回1 此時不能通過n>1判斷是否成功了

        //7. 釋放資源 (先開後放)
        st.close();
        conn.close();
    }

           

DDL執行成功,結果也可能是0

注意:
  • 以後開發很少使用java代碼操作DDL語句

3.4 ResultSet

3.4.1 概述

ResultSet(結果集對象)作用:

  • 封裝了SQL查詢語句的結果。

而執行了DQL語句後就會傳回該對象,對應執行DQL語句的方法如下:

ResultSet  executeQuery(sql):執行DQL 語句,傳回 ResultSet 對象
           

那麼我們就需要從

ResultSet

對象中擷取我們想要的資料。

ResultSet

對象提供了操作查詢結果資料的方法,如下:

boolean next()
  • 将光标從目前位置向前移動一行
  • 判斷目前行是否為有效行
方法傳回值說明:
  • true : 有效行,目前行有資料
  • false : 無效行,目前行沒有資料
xxx getXxx(參數):擷取資料
  • xxx : 資料類型;如: int getInt(參數) ;String getString(參數)
  • 參數
    • int類型的參數:列的編号,從1開始
    • String類型的參數: 列的名稱

如下圖為執行SQL語句後的結果

JDBC-

一開始光标指定于第一行前,如圖所示紅色箭頭指向于表頭行。當我們調用了

next()

方法後,光标就下移到第一行資料,并且方法傳回true,此時就可以通過

getInt("id")

擷取目前行id字段的值,也可以通過

getString("name")

擷取目前行name字段的值。如果想擷取下一行的資料,繼續調用

next()

方法,以此類推。

3.4.2 代碼實作

@Test
    public void testDML() throws SQLException {
        //1. 注冊驅動 省略

        //2. 擷取連接配接
        String url = "jdbc:mysql:///db1?useSSL=false";
        String user = "root";
        String password = "1234";
        Connection conn = DriverManager.getConnection(url, user, password);

        //3. 定義sql
        String sql = "select * from account";

        //4. 擷取Statement對象
        Statement st = conn.createStatement();

        //5. 執行sql
        ResultSet rs = st.executeQuery(sql);

        //6. 處理結果
       /* while (rs.next()){
            //根據列标通路  注意從1開始
            int id = rs.getInt(1);
            String name = rs.getString(2);
            double money = rs.getDouble(3);
            System.out.println(id+" "+name+" "+money);
        }*/
        //根據列名通路最好
        while (rs.next()){
            int id = rs.getInt("id");
            String name = rs.getString("name");
            double money = rs.getDouble("money");
            System.out.println(id+" "+name+" "+money);
        }

        //7. 千萬記得釋放資源
        rs.close();
        st.close();
        conn.close();
    }
           
JDBC-

3.5 案例

  • 需求:查詢account賬戶表資料,封裝為Account對象中,并且存儲到ArrayList集合中
    JDBC-
  1. 定義實體類Account
  2. 查詢資料,封裝到Account對象中
  3. 将Account對象存入ArrayList集合中
  • 代碼實作
/**
 * 查詢account賬戶表資料,封裝為Account對象中,并且存儲到ArrayList集合中
 *  1. 定義實體類Account
 *  2. 查詢資料,封裝到Account對象中
 *  3. 将Account對象存入ArrayList集合中
 */
@Test
public void testResultSet2() throws SQLException {
    //1. 注冊驅動 省略

    //2. 擷取連接配接
    String url = "jdbc:mysql:///db1?useSSL=false";
    String user = "root";
    String password = "1234";
    Connection conn = DriverManager.getConnection(url, user, password);

    //3. 定義sql
    String sql = "select * from account";

    //4. 擷取Statement對象
    Statement st = conn.createStatement();

    //5. 執行sql
    ResultSet rs = st.executeQuery(sql);

    //6. 處理結果
    //根據列名通路最好
    //準備好ArrayList
    List<Account> list = new ArrayList<>();
    while (rs.next()){
        int id = rs.getInt("id");
        String name = rs.getString("name");
        double money = rs.getDouble("money");
        Account account = new Account(id, name, money);
        list.add(account);
    }
    for (Account account : list) {
        System.out.println(account);
    }

    //7. 千萬記得釋放資源
    rs.close();
    st.close();
    conn.close();
}
           
JDBC-

3.6 PreparedStatement

PreparedStatement作用:
  • 預編譯SQL語句并執行:預防SQL注入問題

對上面的作用中SQL注入問題大家肯定不了解。那我們先對SQL注入進行說明.

3.6.1 SQL注入

SQL注入是通過操作輸入來修改事先定義好的SQL語句,用以達到執行代碼對伺服器進行攻擊的方法。

根據要求修改

application.properties

檔案中的使用者名和密碼,檔案内容如下:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=1234
           

在MySQL中建立名為

test

的資料庫

在指令提示符中運作今天資料下的

day03-JDBC\資料\2. sql注入示範\sql.jar

這個jar包。

直接輕按兩下sql.jar也行

JDBC-

此時我們就能在資料庫中看到user表

JDBC-

接下來在浏覽器的位址欄輸入

localhost:8080/login.html

就能看到如下頁面

JDBC-

我們就可以在如上圖中輸入使用者名和密碼進行登陸。使用者名和密碼輸入正确就登陸成功,跳轉到首頁。使用者名和密碼輸入錯誤則給出錯誤提示,如下圖

JDBC-

但是我可以通過輸入一些特殊的字元登陸到首頁。

使用者名随意寫,密碼寫成

' or '1' ='1

JDBC-

也能登陸成功!

這就是SQL注入漏洞,也是很危險的。當然現在市面上的系統都不會存在這種問題了,是以大家也不要嘗試用這種方式去試其他的系統。

那麼該如何解決呢?這裡就可以将SQL執行對象

Statement

換成

PreparedStatement

對象。

3.6.2 代碼模拟SQL注入問題

  • 準備工作
use db1;
-- 删除tb_user表
drop table if EXISTS tb_user;
-- 建立tb_user表
create table tb_user(
		id int,
		username varchar(20),
		password varchar(32)
);

-- 添加資料
insert into tb_user VALUES
(1,'zhangsan','123'),
(2,'lisi','234');


SELECT * from tb_user;
           
@Test
public void testResultSet() throws SQLException {
    String url = "jdbc:mysql:///db1?useSSL=false";
    String user = "root";
    String password = "1234";
    Connection conn = DriverManager.getConnection(url, user, password);

    //模拟使用者輸入:使用者名和密碼
    String name = "hackdshjksdhdjh";
    String pwd = "' or '1'='1"; //可怕的注入  直接修改了sql語句的含義

    //sql語句
    String sql = "select * from tb_user where username = '"+name+"' and password = '"+pwd+"'";
    System.out.println(sql);

    //擷取statement對象
    Statement st = conn.createStatement();

    //執行sql
    ResultSet rs = st.executeQuery(sql);

    //判斷登陸是否成功
    if(rs.next()){//根據name和pwd能查到資料就說明登陸成功
        System.out.println("登陸成功!");
    }else {
        System.out.println("失敗~");
    }

    //千萬記得釋放資源
    rs.close();
    st.close();
    conn.close();
}
           
JDBC-

上面代碼是将使用者名和密碼拼接到sql語句中,拼接後的sql語句如下

從上面語句可以看出條件

username = 'hackdshjksdhdjh' and password = ''

不管是否滿足,而

or

後面的

'1' = '1'

是始終滿足的,最終條件是成立的,就可以正常的進行登陸了。

更為可怕的是,這條sql語句會查出tb_user表的所有資料

JDBC-

接下來我們來學習PreparedStatement對象.

3.6.3 PreparedStatement概述

PreparedStatement作用:
  • 預編譯SQL語句并執行:預防SQL注入問題
  • 擷取 PreparedStatement 對象
    // SQL語句中的參數值,使用?占位符替代
    String sql = "select * from user where username = ? and password = ?";
    // 通過Connection對象擷取,并傳入對應的sql語句
    PreparedStatement pstmt = conn.prepareStatement(sql);
               
  • 設定參數值

    上面的sql語句中參數使用 ? 進行占位,在執行之前肯定要設定這些 ? 的值。

    PreparedStatement對象:setXxx(參數1,參數2):給 ? 指派
    • Xxx:資料類型 ; 如 setInt (參數1,參數2)
    • 參數:
      • 參數1: ?的位置編号,從1 開始
      • 參數2: ?的值
  • 執行SQL語句

    executeUpdate(); 執行DDL語句和DML語句

    executeQuery(); 執行DQL語句

    注意:

    • 調用這兩個方法時不需要傳遞SQL語句,因為擷取SQL語句執行對象時已經對SQL語句進行預編譯了。

3.6.4 使用PreparedStatement改進

@Test
public void testPreparedStatement() throws SQLException {
    String url = "jdbc:mysql:///db1?useSSL=false";
    String user = "root";
    String password = "1234";
    Connection conn = DriverManager.getConnection(url, user, password);

    //模拟使用者輸入:使用者名和密碼
    String name = "hackdshjksdhdjh";
    String pwd = "' or '1'='1"; //可怕的注入  直接修改了sql語句的含義

    /*name = "lisi";
    pwd = "234";*/ //當然也成功啦

    //sql語句
    String sql = "select * from tb_user where username = ? and password = ?";

    //擷取statement對象
    PreparedStatement ps = conn.prepareStatement(sql);//預編譯sql(然後插入值,這樣就不用拼接了)

    //設定?值
    ps.setString(1,name);//設定第一個?的值
    ps.setString(2,pwd);//設定第二個?的值

    //執行sql
    ResultSet rs = ps.executeQuery(); //不用傳入sql了 前面傳過了

    //也列印下sql
    System.out.println(((JDBC4PreparedStatement)ps).asSql());
    //發現整個pwd當做一個字元串處理了('号會被轉義),再也不會修改sql語句了

    //判斷登陸是否成功
    if(rs.next()){//根據name和pwd能查到資料就說明登陸成功
        System.out.println("登陸成功!");
    }else {
        System.out.println("失敗~");
    }

    //千萬記得釋放資源
    rs.close();
    ps.close();
    conn.close();
}
           
JDBC-

特殊字元如

'

被轉義了,再也不會修改sql語句的邏輯了

JDBC-

3.6.5 PreparedStatement原理

PreparedStatement 好處:
  • 預編譯SQL,性能更高
  • 防止SQL注入:将敏感字元進行轉義
JDBC-

Java代碼操作資料庫流程如圖所示:

  • 将sql語句發送到MySQL伺服器端
  • MySQL服務端會對sql語句進行如下操作
    • 檢查SQL語句

      檢查SQL語句的文法是否正确。

    • 編譯SQL語句。将SQL語句編譯成可執行的函數。

      檢查SQL和編譯SQL花費的時間比執行SQL的時間還要長。如果我們隻是重新設定參數,那麼檢查SQL語句和編譯SQL語句将不需要重複執行。這樣就提高了性能。

      sql編譯一次,下次若隻是參數不同就不需要重新編譯,直接換參執行即可,大大提高效率

    • 執行SQL語句

接下來我們通過查詢日志來看一下原理。

  • 開啟預編譯功能

    在代碼中編寫url時需要加上以下參數。而我們之前根本就沒有開啟預編譯功能,隻是解決了SQL注入漏洞。

    (預設是不開的,得手動開才行,否則就是紙上談兵)

  • 配置MySQL執行日志(重新開機mysql服務後生效)

    在mysql配置檔案(my.ini)中添加如下配置

    log-output=FILE
    general-log=1
    general_log_file="D:\mysql.log"
    slow-query-log=1
    slow_query_log_file="D:\mysql_slow.log"
    long_query_time=2
               
JDBC-

沒有權限就先複制到桌面,修改儲存好後再複制回去

JDBC-

重新開機看看日志目錄下是否有這兩個.log檔案

JDBC-
  • java測試代碼如下
/**
 * PreparedStatement原理
 */
@Test
public void testPreparedStatement2() throws SQLException {
    String url = "jdbc:mysql:///db1?useSSL=false&useServerPrepStmts=true";
    String user = "root";
    String password = "1234";
    Connection conn = DriverManager.getConnection(url, user, password);

    //模拟使用者輸入:使用者名和密碼
    String name = "hackdshjksdhdjh";
    String pwd = "' or '1'='1"; //可怕的注入  直接修改了sql語句的含義

    //sql語句
    String sql = "select * from tb_user where username = ? and password = ?";

    //擷取statement對象
    PreparedStatement ps = conn.prepareStatement(sql);//預編譯sql(然後插入值,這樣就不用拼接了)

    //設定?值
    ps.setString(1, name);//設定第一個?的值
    ps.setString(2, pwd);//設定第二個?的值
    //執行sql
    ResultSet rs = ps.executeQuery(); //不用傳入sql了 前面傳過了
    if (rs.next()) System.out.println("登陸成功!");
    else System.out.println("失敗~");

    //再執行一次sql
    ps.setString(1, "lisi");//設定第一個?的值
    ps.setString(2, "234");//設定第二個?的值
    rs = ps.executeQuery(); //不用傳入sql了 前面傳過了

    //判斷登陸是否成功
    if (rs.next()) {//根據name和pwd能查到資料就說明登陸成功
        System.out.println("登陸成功!");
    } else {
        System.out.println("失敗~");
    }

    //千萬記得釋放資源
    rs.close();
    ps.close();
    conn.close();
}
           

上面的代碼執行了兩次sql: 一次注入失敗,一次正常登陸成功

JDBC-
  • 執行SQL語句(執行上面java代碼就是執行sql),檢視

    E:\log\mysql.log

    日志如下:
MySQL, Version: 5.7.24-log (MySQL Community Server (GPL)). started with:
TCP Port: 0, Named Pipe: (null)
Time                 Id Command    Argument
2023-02-24T14:12:46.862195Z	    2 Connect	root@localhost on db1 using TCP/IP
2023-02-24T14:12:46.864956Z	    2 Query	/* mysql-connector-java-5.1.48 ( Revision: 29734982609c32d3ab7e5cac2e6acee69ff6b4aa ) */SELECT  @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results, @@character_set_server AS character_set_server, @@collation_server AS collation_server, @@collation_connection AS collation_connection, @@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout, @@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS net_buffer_length, @@net_write_timeout AS net_write_timeout, @@performance_schema AS performance_schema, @@query_cache_size AS query_cache_size, @@query_cache_type AS query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone, @@transaction_isolation AS transaction_isolation, @@wait_timeout AS wait_timeout
2023-02-24T14:12:46.875712Z	    2 Query	SET character_set_results = NULL
2023-02-24T14:12:46.875885Z	    2 Query	SET autocommit=1
2023-02-24T14:12:46.886850Z	    2 Prepare	select * from tb_user where username = ? and password = ?
2023-02-24T14:12:46.887324Z	    2 Execute	select * from tb_user where username = 'hackdshjksdhdjh' and password = '\' or \'1\'=\'1'
2023-02-24T14:12:46.887585Z	    2 Execute	select * from tb_user where username = 'lisi' and password = '234'
2023-02-24T14:12:46.887825Z	    2 Close stmt	
2023-02-24T14:12:46.889170Z	    2 Quit	
           

上面日志中,前面是通路資料庫連接配接的一些步驟

後面, 倒數第五行中的

Prepare

是對SQL語句進行預編譯。倒數第四行和倒數第三行是執行了兩次SQL語句,而第二次執行前并沒有對SQL進行預編譯。

(去掉預編譯的代碼

&useServerPrepStmts=true

日志裡就沒有Prepare了)

小結:
  • 在擷取PreparedStatement對象時,将sql語句發送給mysql伺服器進行檢查,編譯(這些步驟很耗時)
  • 執行時就不用再進行這些步驟了,速度更快
  • 如果sql模闆一樣,則隻需要進行一次檢查、編譯

4,資料庫連接配接池