JDBC是Java連接數據庫的標準,為了兼容大部分數據庫,Java提出了JDBC標準,通過這個標準,讓各個數據庫提供實現支持,這樣實現一處編碼,處處運行的Java特性。
習慣了ORM框架,卻忘記了原本的JDBC,所以我覺得有必要復習來夯實一下基礎。
0x00 JDBC 歷史
JDBC是Sun公司為了能夠讓SQL訪問統一的一套純JAVA API設計的一套接口,這種接口是遵循了微軟的ODBC API模式。其驅動實現是各家數據庫供應商編寫的,通過JDBC API可以通過驅動實現數據庫通信。
0x01 鏈接數據庫回顧
基本Web常用的數據庫都是有供Java鏈接的驅動,
那么如何使用JDBC?
寫個Demo
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
/**
* Created by zing on 2017/3/7.
*/
public class JDBCDemo {
private void testJDBC() throws ClassNotFoundException, IOException, SQLException {
//注冊驅動
Class.forName("com.mysql.jdbc.Driver");
//JDBC使用類似URL的數據源描述
String url = "jdbc:mysql://localhost:3306/demo";//忽略
//但是我們一般不會直接這樣寫死。而是使用配置來描述數據源,用戶名,密碼
Properties props = new Properties();
FileInputStream propertiesFile = new FileInputStream("JDBC.properties");
props.load(propertiesFile);
propertiesFile.close();
String DriverStr = props.getProperty("jdbc.Driver");
String urlStr = props.getProperty("jdbc.url");
String userName = props.getProperty("jdbc.name");
String passcode = props.getProperty("jdbc.passworld");
//打開數據庫鏈接
Connection connection = DriverManager.getConnection(urlStr,userName,passcode);
//執行SQL
Statement sta = connection.createStatement();
//executeUpdate可以返回數據庫更新的行數
int efactRow = sta.executeUpdate("UPDATE USER SET Permition = 'admin' WHERE username = 'Zing'");
//executeQuery可以返回一個查詢的結果集,這個集合的迭代器略有不同Iterator,沒有hasNext方法,初始是,指針在數據前,必須調用next方法才能讀取第一行數據
ResultSet resultSet = sta.executeQuery("SELECT * FROM USER ;");
while (resultSet.next()){
//當前行獲取第一欄的值,具體類型需要看數據庫實現
resultSet.getString(1);
}
//關閉語句
sta.close();
//關閉結果集
resultSet.close();
//關閉數據庫連接
connection.close();
/*
一般情況下,關閉的操作會放在catch語句的finally塊中,catch處理數據庫異常,finally來關閉連接
*/
}
}
上面寫的是大雜燴,一般會將獲取連接抽取成一個方法,異常也會捕獲,并在try/catch/finally中的finally塊中,關閉數據庫連接。
API用法可以看看java.sql.Connection
,java.sql.Statement
,java.sql.ResultSet
,這樣,基本的操作就可以了然了。
boolean execute(String sql) throws SQLException;
這個方法可以執行任何SQL,返回執行是否成功。
0x02 預編譯SQL
PrepareStatement,一個可以讓數據庫預編譯SQL的API。
并不是所有的SQL都是寫死的,例如:
SELECT * FROM UserAccount Where Name =
根據名稱來查找用戶,這里的名字自然是用戶自己定義的,如果用Statement,則應該這么寫
public void findUserByName(String name){
Statement sta = connection.createStatement();
String findByName = "SELECT * FROM USER WHERE Name=' "+name+" ';";
ResultSet resultSet = sta.executeQuery(findByName);
}
如果將name交給普通用戶來輸入,則沒什么問題,但是 如果交給黑客,name他會輸入 小明' OR '1' = '1
,這樣語句拼接后就會變成
SELECT * FROM USER WHERE Name=' 小明' OR '1' = '1';
這一句就會把數據庫所有的用戶全部查出來了,很嚴重的注入漏洞,基本就會被脫庫了。
所以Java JDBC定義的預編譯SQL的API。
上例子:
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
public class JDBCDemo {
private void testJDBC() {
Connection connection = null;
Statement sta = null;
ResultSet resultSet = null;
PreparedStatement preSta = null;
try {
connection = getConnection();
//執行SQL
String findByName = "SELECT * FROM USER WHERE Name=?;";
preSta = connection.prepareStatement(findByName);
preSta.setString(1,"Zing");
resultSet = preSta.executeQuery();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
/*
一般情況下,關閉的操作會放在catch語句的finally塊中,catch處理數據庫異常,finally來關閉連接
*/
//關閉語句
try {
preSta.close();
//關閉結果集
resultSet.close();
//關閉數據庫連接
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public Connection getConnection() throws ClassNotFoundException, IOException, SQLException {
//注冊驅動
Class.forName("com.mysql.jdbc.Driver");
//JDBC使用類似URL的數據源描述
//但是我們一般不會直接這樣寫死。而是使用配置來描述數據源,用戶名,密碼
Properties props = new Properties();
FileInputStream propertiesFile = new FileInputStream("JDBC.properties");
props.load(propertiesFile);
propertiesFile.close();
String DriverStr = props.getProperty("jdbc.Driver");
String urlStr = props.getProperty("jdbc.url");
String userName = props.getProperty("jdbc.name");
String passcode = props.getProperty("jdbc.passworld");
return DriverManager.getConnection(urlStr, userName, passcode);
}
}
順便重構了之前的代碼。
我們用 ?
占位,留下可變參數的位置,后來再用setString(int parameterIndex, String x)
這個方法將數據填充進SQL,這樣,如果參數含有SQL關鍵字時,就不能通過編譯,查不到結果。可以避免SQL注入。
preSta.setString(1,"Zing");表示,在第一個?
處設置參數為Zing
當然參數是數字,日期時,可以使用,
void setDouble(int parameterIndex, double x) throws SQLException
setDate(int parameterIndex, java.sql.Date x) throws SQLException;
等方法,根據不同類型設置參數。
0x03 數據庫類型與轉義
數據庫類型和Java類型是有一點不一樣的,但是JDBC定義了其中的大部分類型,這里不一一列舉
JDBC中的轉義是為了讓Java訪問數據庫時,得到普遍的支持。一般用于下列特性
- 時間日期的字面常亮
- 標量函數調用
- 存儲過程調用
- 外連接查詢
- LIKE子句中轉義字符
數據庫的日期轉換成Java的日期,是通過ISO8601標準衡定并相互轉換的
d表示DATE、t表示TIME、ts表示TIMESTANP
{d '2017-01-22'}
{t '19:30:29'}
{ts '2017-01-22 19:30:29.989'}
標量函數是獲取一個數值的函數,一般調用時嵌入標準函數名和參數,這個很少見到有人使用的,就不舉例了。
存儲過程,是數據庫自建的存儲方式,不同的數據庫存儲過程基本不一樣,要調用存儲過程,需要用call來進行轉義
{call PROC01(?,?)}
{call PROC02}
如果你不明白什么存儲過程,可以看看數據庫相關的資料。
外連接,就是Outter Join,借用核心卷II中的例子
SELECT * FROM {oj Books LEFT OUTER JOIN Publishers ON Books.Publish_ID = Publishers.Publish_ID }
這條語句表示查詢找不到出版商的書,相反如果是RIGHT OUTER JOIN
則會查詢出沒有出版書的出版商,如果需要查到全部,則用FULL OUTER JOIN
。這里用轉義是因為有些數據庫實現不太統一。
Like子句轉義,是因為下劃線和百分號在Like條件里是特殊的含義,需要用轉義來表示
SELECT * FROM User WHERE Name LIKE %!_%ming {escape '!'}
{escape '!'}表示將!定義為轉義符號,!_表示字面量下劃線
0x04 事務
為了保證數據和業務邏輯的完整性,我們可以將一系列的SQL語句構建成一個事物,當所有語句都順利執行的時候,事務可以被提交。但是如果中途被阻礙,則數據會被回滾,將數據恢復成執行前的樣子。
首先需要關閉數據庫自動提交
connection.setAutoCommit(false);
然后根據實際業務執行多條UPDATE INSERT DELETE語句
statement.executeUpdate("SQL1");
statement.executeUpdate("SQL2");
statement.executeUpdate("SQL3");
當所有語句順利執行后,調用
connection.commit();
如果遇到異常或錯誤,則可以調用
connection.rollback();
其中JDBC支持事務保存點和批量更新
保存點:將事務的某一階段設置為保存點后,可以控制回滾時,恢復到這個保存點的數據。從而更加精確的控制回滾操作
批量更新就是將大量數據一次性存入,或修改大量數據時使用的。兩個??:
statement.executeUpdate("SQL1");
Savepoint step1 = connection.setSavepoint();
statement.executeUpdate("SQL2");
if(something==false){
connection.rollback(step1);
}
String updateSQL = "……";
statement.addBatch(updateSQL);
while(needUpdate){
command = "……"+"updateSQL2"
statement.addBatch(updateSQL);
}
//批量執行
int effectRows = statement.executeBatch();
批量執行中一定不能有查詢語句,否則會拋出異常。
0x05 文件查詢和存入數據庫
不建議這么搞,數據庫存入太多大文件會導致數據庫龐大,備份和恢復的成本將增加。
在數據庫中,二進制大對象稱為Blob,字符型大對象為Clob
這里演示一下查詢和存儲
//讀取
PreparedStatement preparedStatement01 = connection.prepareStatement("SELECT picture FROM PictureTab WHERE picName=?;");
preparedStatement01.setString(1,"superman");
ResultSet rs = preparedStatement01.executeQuery();
if(rs.next()){
Blob picBlob = rs.getBlob(1);
Image pic = ImageIO.read(picBlob.getBinaryStream());
}
//存儲
Blob pictureBlob = connection.createBlob();
int offset = 0;
OutputStream outStram = pictureBlob.setBinaryStream(offset);
ImageIO.write(pictureBlob,"PNG",outStram);
PreparedStatement preparedStatement02 = connection.prepareStatement("INSERT INTO PictureTab VALUE (?,?);");
preparedStatement02.setString(1, "SuperMan");
preparedStatement02.setBlob(2,pictureBlob);
preparedStatement02.executeUpdate();
0x06 其他一些概念
- 元數據:數據庫的結構和表信息等描述數據庫結構和組成部分的數據
- 多結果集:一次查詢,使用多個Select SQL語句是,會得到一個多結果集
- 可滾動結果集:可以向前,向后查詢的結果集,之前的只能用Next向后查詢,使
Statement stat = Connection.createStatement(ResultSet.TYPE_SCROLL_INSENSTIVE , ResultSet.CONCUR_READ_ONLY )
在獲取結果集的時候,會變成一個可滾動集。
- 獲取數據庫生成鍵值
statemwnt.getGeneratedKeys();
- 行集 RowSet接口繼承了ResultSet,但不需要長時間占用數據庫鏈接。
love&peace
我的博客:https://micorochio.github.io/
轉載請注明出處:https://micorochio.github.io/2017/03/10/basic-of-java-JDBC/。