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方法行為的改變體現的是裝飾器模式