1. 程式人生 > >java學習day24~25--JDBC

java學習day24~25--JDBC

基本概念
java 資料庫連線, 是java提供的一套api介面,以一種一致的方式,訪問不同的資料庫(mysql,oracle,sqlserver)

java.sql.*
   java.sql.Driver 驅動(如何連線資料庫)
   java.sql.Connection 連線(代表java程式和資料庫之間連線通道)
   java.sql.Statement 執行sql語句
   java.sql.PreparedStatement
   java.sql.CallableStatement
   java.sql.ResultSet 結果集 代表的是從資料庫查詢結果
   java.sql.DriverManager 工具類,用來獲取Connection
   java.sql.SQLException 代表執行sql過程中出現的異常
   
具體的實現由資料庫廠商來提供

MySQL Connector/J 是mysql提供的針對jdbc介面的實現,以jar包方式提供

idea 中加入mysql驅動jar包

使用jdbc程式設計的步驟
1) 載入驅動 (在新版的jdbc中可以省略此步驟)
2) 建立連線,建立Connection物件
3) 建立Statement 物件 
4) 執行sql語句(執行增刪改或查詢)
5) 關閉釋放資源

重要介面API
1 ResultSet
.next() 方法,移動到結果集的下一條記錄,如果返回true表示有下一條記錄,否則返回false

getXXX(int 列下標) 用來獲取結果集中某一列的資料,其中XXX為資料型別,如果是字串使用 getString 如果是整數,使用getInt ... ,列下標從1開始

getXXX(int 列名) 用來獲取結果集中某一列的資料,其中XXX為資料型別,如果是字串使用 getString 如果是整數,使用getInt 

import java.sql.*;

public class JDBCTest1 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        //1.載入驅動(在新版的jdbc中,可以省略此步驟)  只需要執行一次即可
        //Class.forName("com.mysql.jdbc.Driver");   //mysql 5.1
        Class.forName("com.mysql.cj.jdbc.Driver");

        //2.建立連線,建立Connection物件
//        jdbc:mysql: 稱為連線協議
//        localhost:MySQL伺服器的ip地址
//        3306:連線埠號
//        test:資料庫的名稱
//        serverTimezone=GMT%2B8   設定連線時區與資料庫伺服器一致(連線8.0mysql時新增)
        String url = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useSSL=false";
        String username = "root";
        String password = "007";
        Connection conn = DriverManager.getConnection(url, username, password);

        //3.建立Statement物件
        Statement stmt = conn.createStatement();
        //4.執行sql語句(執行增刪改查功能)
        //executeUpdate()返回的是一個整數,這個數代表增刪改影響的記錄行數
        stmt.executeUpdate("insert into student(sname, birthday, sex) values ('貂蟬', '1001-5-23', '女'),('王昭君','0809-7-16', '女'),('楊玉環','1305-9-17', '女')");   //用於執行insert,update, delete語句
        stmt.executeQuery("select * from student");   //用來執行select語句,進行查詢
//        stmt.execute("sql語句);

        //結果集物件
        ResultSet rs = stmt.executeQuery("select sname, sex from student");
        //rs.next()返回的是一個boolean值,表示是否有下一條記錄
        while(rs.next()) {
            String sname = rs.getString(1);   //getXXX方法的引數,代表第幾列,從1開始
            String sex = rs.getString(2);
            System.out.println("sname--->" + sname + "\tsex--->" + sex);
        }
        //關閉釋放資源,先開啟的資源最後關閉
        rs.close();
        stmt.close();
        conn.close();


//select * from student;

    }
}

2 PreparedStatement
要求SQL語句中的值使用`?`佔位符來佔位,然後通過一系列的setXXX
方法來給`?`賦值,XXX根據值的型別決定,例如?為字串呼叫setString,?為整數呼叫setInt
setXXX(?下標, 值) 下標也是從1開始

關於SQL注入攻擊:
這是一種黑客常用於"填空遊戲",將使用者名稱或者密碼加上一個絕對為真的條件,將登入變為無賬號密碼
例如:
某個網站的登入驗證的SQL查詢程式碼為:
strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
惡意填入
userName = "1' OR '1'='1";  和  passWord = "1' OR '1'='1";  時,將導致原本的SQL字串被填為
strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"
也就是實際上執行的SQL命令會變成下面這樣的
strSQL = "SELECT * FROM users;"
因此達到無賬號密碼,亦可登入網站。所以SQL注入攻擊被俗稱為黑客的填空遊戲

此時用PreparedStatement,可以防止這類事件的發生

賬號密碼登入沒有使用PreparedStatement之前的程式碼

public static boolean login(String username, String password) throws SQLException {
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        Statement stmt = conn.createStatement();
        //拼接字串的方法生成了sql語句
        String sql = "select * from user where username = '" +username+ "' and password = '" +password+"'";
        System.out.println(sql);
        ResultSet rs = stmt.executeQuery(sql);
        //判斷rs是否有下一條記錄
        boolean r = rs.next();
        rs.close();
        stmt.close();
        conn.close();
        //如果有下一條記錄,表示找到了,使用者密碼都正確
        return r;
    }

使用之後的程式碼

import java.sql.*;

public class LoginMysql2 {
    static final String URL = "jdbc:mysql://localhost:3306/test3?serverTimezone=GMT%2B8&useSSL=false";
    static final String USERNAME = "root";
    static final String PASSWORD = "root";

    //使用PreparedStatement預編譯的Statement,可以避免SQL注入攻擊漏洞,且讓程式碼的可讀性更好
    public static boolean login(String username, String password) throws SQLException {
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        PreparedStatement stmt = conn.prepareStatement("select * from USER where username = ? and password = ?");
        //給sql語句中的?賦值
        stmt.setString(1, username); //將username變數賦值給sql語句中的第一個?,?從下標從1開始
        stmt.setString(2, password); //不會把其中的關鍵字,如or進行解析,而是把它當做普通值進行處理
        ResultSet rs = stmt.executeQuery();
        boolean r = rs.next();
        rs.close();
        stmt.close();
        conn.close();
        return r;
    }
}

程式設計的編碼規範:
像連線字串,資料庫使用者名稱密碼都可以定義為靜態常量,靜態常量名大寫,如果有多個單詞中間用下劃線分隔

由於每一次都需要建立各種物件,並進行異常的處理,以及輸入URL等一系列麻煩的操作,我們可以將這些東西做成一個工具包,在需要的時候進行直接呼叫,程式碼如下

import java.sql.*;

public class Utils {
    static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useSSL=false";
    static final String USERNAME = "root";
    static final String PASSWORD = "007";

    public static Connection getConnection() throws SQLException {
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        return conn;
    }

    public static void close(ResultSet rs, PreparedStatement stmt, Connection conn) {
        if(rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if(stmt != null) {
            try {
                stmt.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

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

    public static void close(PreparedStatement stmt, Connection conn) {
        close(null, stmt, conn);
    }
}

 事務
把多條sql語句看做一個整體執行,ACID(原子性,一致性,隔離性,永續性)
Connection conn = ...
conn.setAutoCommit( true | false ); // 自動提交, 預設值是true
所謂的自動提交,就是在每條執行的增刪改sql語句之後,由jdbc自動加一條commit語句,讓事務提交(更改生效)

stmt.executeUpdate("insert into..."); // commit;
stmt.executeUpdate("update ..."); // commit;
stmt.executeUpdate("delete ..."); // commit;當呼叫 conn.setAutoCommit(false) 含義就是讓事務手動提交, 後續的多條sql就視為一個事務
try {
    stmt.executeUpdate("insert into...");  // ok    
    stmt.executeUpdate("update ..."); // 異常
    conn.commit(); // 手動提交事務(更改生效)
} catch(Exception e) {
    conn.rollback(); // 手動回滾事務
}
例子:

import java.sql.Connection;
import java.sql.PreparedStatement;

public class CommitTest1 {
    /**
     * 事務
     * 把多條sql語句看做一個整體執行,ACID(原子性,一致性,隔離性,永續性)
     * 一般情況下,conn.setAutoCommit(true)中的true是預設值,為自動提交
     * 所謂的自動提交,就是在每條執行的增刪改sql語句之後,由jdbc自動增加一條commit語句,讓事務提交(更改生效)
     * 當將其中的true變為false,含義就是讓事務手動提交,後續的多條sql就視為一個事務
     */

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement stmt = null;

        try {
            conn = Utils.getConnection();
            //讓事務手動提交
            conn.setAutoCommit(false);
            stmt = conn.prepareStatement("delete from student where sid = ?");
            stmt.setInt(1, 1017);
            stmt.executeUpdate();
            System.out.println("delete from student where sid = 1017");

            stmt.setInt(1, 1009);
            stmt.executeUpdate();
            System.out.println("delete from student where sid = 1009");
            //int i = 10/0;

            //所有操作都成功了之後,再commit
            conn.commit();

        } catch (Exception e) {
            e.printStackTrace();
            if(conn != null) {
                try{
                    //一旦有異常,回滾之前的操作
                    conn.rollback();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        } finally {
            Utils.close(stmt, conn);
        }
    }
}

效率提升

1 充分利用預編譯sql
  1) 在jdbc中使用PreparedStatement介面
  2) 對於mysql 需要在連線字串上加兩個引數: &useServerPrepStmts=true&cachePrepStmts=true

資料庫有著非常艱苦的工作,它們接收各種客戶端發出的sql查詢,並儘可能快的查詢並返回結果,處理statement是一個開銷昂貴的操作,不過有了PreparedStatement,可以將開銷降到最低,儘量優化

資料庫手啊哦一個statement之後,資料庫會對其進行解析,判斷錯誤,一旦正確解析,資料庫會選出執行的最優途徑,建立方案並執行,但這個存取方案的生成會佔用很多CPU,理想狀況是:當我們一次傳送多個statement到資料庫,資料庫應該對statement的存取方案進行重用,如果方案曾經生成過,這將減少CPU的使用率

/*
在這裡快取不會被使用,因為每一次迭代都會發送一條包含不同的sql語句的statement給資料庫,並且每一次迭代都會生成一個新的存取方案
*/
for (int i = 0; i < 1000; i++)  {
    PreparedStatement ps = conn.prepareStatement("select a,b from t where c = " + i);
    ResultSet rs = Ps.executeQuery();
    rs.close();
    ps.close();
}
/*
這樣就具有了更好的效率,這個statement傳送給資料庫的是一條帶有引數“?”的SQL語句。這樣每次迭代會發送相同的statement到資料庫,只是引數“c=?”不同。這種方法允許資料庫重用statement的存取方案,這樣就具有了更好的效率。這可以讓你的應用程式速度更快,並且使用更少的CPU,這樣資料庫伺服器就可以為更多的人提供服務
*/
PreparedStatement ps = conn.prepareStatement("select a,b from t where c = ?");
for (int i = 0; i < 1000; i++)  {
    ps.setInt(1, i);
    ResultSet rs = ps.executeQuery();
    rs.close();
    ps.close();
}
 

2 批處理(對增刪改提升,對查詢無效)
  注意:
  對於mysql 要啟用批處理功能需要新增引數: &rewriteBatchedStatements=true

package jdbc;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class TestJDBCBigTable {

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            conn = Utils.getConnection();
            long start = System.currentTimeMillis();
            stmt = conn.prepareStatement("insert into bigtable(id,name) values(?,?)");
            int max = 500;
            for (int i = 0; i < 1000000; i++) {
                stmt.setInt(1,i);
                stmt.setString(2,"aaa");
//                stmt.executeUpdate(); // 呼叫一次exeucteUpdate方法,就會跟資料庫伺服器互動一次
                stmt.addBatch(); // 將insert語句加入至批處理包
                if( (i+1) % max == 0 ) { // 批處理包滿500發一次
                    stmt.executeBatch();
                }
            }
            stmt.executeBatch(); // 將批處理包中所有sql一次性發送給伺服器
            long end = System.currentTimeMillis();
            System.out.println(end-start);
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            Utils.close(stmt, conn);
        }
    }
}

3. fetchSize 抓包大小(跟查詢有關)
mysql 預設查詢,會把查詢到的所有記錄都包裝在ResultSet中返回, 當結果記錄太多時,效率,記憶體都會受到影響
需要新增: &useCursorFetch=true&defaultFetchSize=100  來啟用基於遊標的ResultSet查詢
  
4 .從資料庫角度考慮效率提升 
對於查詢,建立索引:
create index 索引名字 on 表名(列名);
create index idx_bigtable_id on bigtable(id);
1) 對經常做查詢的列建索引
2) 索引會影響增刪改效率,如果查詢操作遠遠高於增刪改,適合建立索引
3) create table 表名( id int primary key )
    主鍵列上會自動建立索引
4) 外來鍵列上建立了索引,會有助於表連線的效率
5) 查詢列如果應用了函式,則不會利用索引
 select * from bigtable where abs(-2) = 263161;
    -2
    -1
    1
    2
    3
6) 模糊查詢
select * from 表 where 列 like '%...'  ; /*不會走索引*/
select * from 表 where 列 like 'aaaa%...'  ; /*會走索引*/

5 資料庫連線池
預先建立一些資料庫連線,用的時候從連線池借,每次使用完連線,要還回連線池,而不是真正關閉.
連線池的介面和實現:
javax.sql.DataSource;  // 介面
DataSource.getConnection() // 從連線池獲取一個空閒的連線, 池連
DriverManager.getConnection() // 直接與資料庫建立新的連線, 直連

實現:
各個應用程式伺服器
Tomcat 內建了連線池
weblogic 內建了連線池
websphere 內建了連線池

第三方的獨立連線池實現
apache dbcp 
c3p0
alibaba druid 德魯伊
  
小結:
連線池的作用:1. 實現連線的重用 2. 限制了連線的上限,不至於把整個資料庫拖垮
涉及到的設計模式:1) 連線的重用體現了享元模式  2) close方法行為的改變體現的是裝飾器模式