想要更好的閱讀體驗,可以轉我的個人博客: nonlinearthink
JDBC (Java Database Connectivity) ,屬於 Java 應用編程中比較基礎的一塊,我們會從最基本的開始,由淺入深地解釋 JDBC 中的各種問題。
連接數據庫
驅動初始化
基於 Class.forName 的初始化
想要開始 JDBC 編程,第一步是需要把 數據庫驅動程序
的代碼加載進來。
可以利用 Class.forName
函數,它原本的功能是返回一個 類或者接口的 Class 對象 ,也就是相當於初始化了一個類,一般用它執行這個類的靜態代碼段。
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
這樣,com.mysql.jdbc.Driver
類中的靜態代碼段就會被執行,進行初始化。
不過,驅動初始化還有另外一種寫法。
基於 registerDriver 的初始化
我們知道 Class.forName
是執行類的靜態代碼段,那我們把 com.mysql.jdbc.Driver
裏面的靜態代碼段照着樣子抄一遍不也可以實現初始化嗎?
這是 com.mysql.jdbc.Driver
靜態代碼段的源代碼:
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
這段代碼可以完全代替上一小節的代碼,我們可以認爲 DriverManager.registerDriver(new Driver())
和 Class.forName
的寫法是等效的。
什麼是
數據庫驅動程序
?JDK 提供了一個 JDBC 的接口(Interface)。但是,因爲是接口,所以還是沒有實現的,每個數據庫的廠商需要自己實現這個接口,這樣用戶才能正常調用。
以 mysql 爲例,下載地址: dev.mysql.com/downloads/connector/j
接口變動
mysql5 和 mysql6 的驅動程序接口有區別,從
com.mysql.jdbc.Driver
換成了com.mysql.cj.jdbc.Driver
,再往上的版本同 mysql6 。
建立連接
數據庫打開後會在本地開一個端口,運行進程,我們可以通過這個端口的 URL 來訪問數據庫。
當然,還需要數據庫的用戶名和密碼。
java.sql.DriverManager.getConnection(url, user, password);
JDBC 的 URL 格式: jdbc:[數據庫連接名]://localhost:[端口號]/[數據庫名]
- 數據庫連接名
mysql、sqlserver - 端口號
3306(mysql)、1433(sqlserver) - 數據庫名
業務相關的數據庫名,自定義
一個 JDBC 連接數據庫的例子:
try {
java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/booklib",
"root", "123456");
} catch (SQLException e) {
e.printStackTrace();
}
JDBC 基礎
Statement
Statement 可以根據給出的一條 SQL 字符串,然後調用運行。
借用上一節連接得到的 conn
對象,它有一個 createStatement
函數,可以創建一個 Statement。
try {
java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/booklib",
"root", "123456");
java.sql.Statement st = conn.createStatement();
} catch (SQLException e) {
e.printStackTrace();
}
execute、executeQuery、executeUpdate
創建完 Statement,就要把 SQL 語句交給 Statement 對象去執行了。
executeQuery
一般如果是運行 查詢(select) 語句,推薦使用 第一節中獲得的conn
對象的 executeQuery
函數,executeQuery
只能返回單個結果集,但是應對大部分的查詢已經足夠。
添加下面的代碼到上面的 try 代碼塊中。
String sql = "select * from BeanBook";
st.executeQuery(sql);
executeUpdate
如果你需要運行 insert
、update
、delete
等等語句,則可以使用 executeUpdate
函數,它不會返回結果集,但是會返回一個整數,代表受到影響的行數,如果是 0,代表了你沒有改變任何數據庫的內容,即調用失敗了。
使用 executeUpdate
:
String sql = "delete from BeanBook where price>50";
st.executeUpdate(sql);
execute
execute
是更加通用和強大的函數,但是它也比較複雜。它的返回值類型有很多。
execute
不僅可以做到 executeQuery
能做的事,也能做到 executeUpdate
能做到的事情。而且,它還能返回多個結果集。
正因爲如此,它一般被用在一些執行未知 SQL 字符串的情況下,如果 SQL 語句能夠確定,請儘可能不用 execute
。
它的返回值比較複雜,我們一般使用 getResultSet
和 getUpdateCount
獲取,而不是直接把 st.execute(sql);
的結果拿來。
以下兩段代碼和上面兩小節的 demo 的效果是一樣的。
//代碼段1
String sql = "select * from BeanBook";
st.execute(sql);
st.getResultSet();
//代碼段2
String sql = "delete from BeanBook where price>50";
st.execute(sql);
st.getUpdateCount();
ResultSet
前面一直沒提 ResultSet,ResultSet 就是 executeQuery
的返回值。
舉個例子,使用 ResultSet 遍歷 SQL 的結果:
String sql = "select * from BeanBook";
java.sql.ResultSet rs = st.executeQuery(sql);
while (rs.next()) {
System.out.println(rs.getString(1) + "\t" + rs.getString(2) + "\t" + rs.getDouble(4));
}
ResultSet 通過 next
函數來遍歷,next
從一條記錄跳轉到下一條記錄。
getString
、getDouble
等等函數接受一個數字 n 作爲參數,獲得當前記錄的第 n 個屬性的值,並對這個值進行轉換。
比如 getString(1)
獲取第一個屬性,轉換成 String
類型;getDouble(4)
獲取第四個屬性,轉換成 Double
類型。
下面是一些 get 函數 (不全):
- 原始類型相關
getString、getBoolean、getByte、getShort、getInt、getLong、getFloat、getDouble - 日期相關
getDate、getTime、getTimestamp
PreparedStatement
通過 conn.prepareStatement
可以來創建一個 PrepareStatement 對象 (conn 是一個 java.sql.Connection
)。
但是這個函數必須要給出一個 SQL 語句作爲參數。
這裏也可以看出 PrepareStatement 與 Statement 的一個比較大的區別。Statement 可以一直被複用,但是 PrepareStatement 每執行一次 SQL,都要創建新的 PrepareStatement。
//Statement
java.sql.Statement st = conn.createStatement();
Int limit = 50;
String sql = "delete from BeanBook where price>50";
st.executeUpdate(sql);
String sql2 = "select * from BeanBook";
st.executeQuery(sql2);
//PrepareStatement
String sql = "delete from BeanBook where price>50";
java.sql.PreparedStatement pst = conn.prepareStatement(sql);
st.executeUpdate();
String sql2 = "select * from BeanBook";
java.sql.PreparedStatement pst2 = conn.prepareStatement(sql2);
st.executeQuery();
PreparedStatement 和 Statement 一樣是用來執行 SQL 語句的,但是 Statement 有很多問題。
字符串拼接問題
假如我們希望可以動態地設置 SQL 語句,比如,動態改變 where
從句的條件,在 Statement 中,我們需要這樣寫:
java.sql.Statement st = conn.createStatement();
Int limit = 50;
String sql = "delete from BeanBook where price>"+limit;
st.executeUpdate(sql);
PrepareStatement 允許一種可讀性非常好的參數設置語法:
String sql = "delete from BeanBook where price>?";
java.sql.PreparedStatement pst = conn.prepareStatement(sql);
pst.setInt(1, 50);
st.executeUpdate(sql);
這個 ?
語法可不止可讀性好,還有一個更重要的是,它支持了 預編譯
,這在我們接下來提到的性能問題中會被具體討論。
還需要提一下的是
setInt
等等的set
相關的函數,前面已經提過get
函數了,基本上把get
改成set
就行了。
性能問題
Statement 的想法是對象只需要創建一次,後續只要傳入不同的 SQL 就行了。但是在面對重複都比較高的工作的時候,這可能並不是很好。
比如,我執行了一次 insert into Beanbook(barcode, bookname, pubid, price, state) values(1, 'Java', 3, 56, '在庫')
,
現在我稍微變一下,要插入 insert into Beanbook(barcode, bookname, pubid, price, state) values(2, 'C++', 2, 34, '在庫')
。
這兩條命令幾乎一樣,但是我卻要編譯兩次!!!
我們來看看 PrepareStaement,我們完全可以使用 ?
語法,創建一次模版,因爲存在預編譯機制,當我們第二次插入的時候節省了一次編譯的開銷。
也就是說,在可以使用 ?
語法替換的一系列 SQL 操作中,使用 PrepareStatement 將會節省一大筆開銷。
SQL 注入式攻擊
SQL 注入式攻擊其實很簡單,這是完全可以避免的,但是使用 Statement 的時候你要格外小心。
假設不懷好意的用戶對你的數據庫請求刪除一些合法的東西,比如刪除 bookname
等於 'Java'
的書,但是他傳給你的字符串做了一些手腳:
//用戶的數據
String name="'Java' OR price>0";
//你的代碼
String sql = "delete from BeanBook where bookname="+name;
好了,你完了,因爲字符串拼接的時候,後面的 OR price>0
沒有被當作是 bookname
的一部分,而是被當成是 SQL 命令的一部分!!! 在這裏,你的數據庫已經被清空了。
但是使用 ?
語法你完全不用擔心,因爲 PrepareStatement 是預編譯的,後面只會插入數據,插入的內容不會被當作是 SQL 命令。
close
在數據庫的最後不能忘記,關閉連接。
在原先代碼的基礎上,在最後的 finally 語句塊中加入 close()
函數。
public static void main(String[] args) throws SQLException {
java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/booklib", "root",
"123456");
try {
conn.setAutoCommit(false);
String sql = "select * from BeanBook";
java.sql.PreparedStatement pst = conn.prepareStatement(sql);
pst.executeQuery();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null)
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
JDBC時間類型的處理
從之前的 get
函數中,我們看到了日期相關的 API。
JDBC 中一共有三種時間類型:
- Date
- Time
- Timestamp
System.currentTimeMillis()
這個函數可以獲得 系統當前的一個時間戳。
時間戳是 1970年1月1日0點0分 到現在的毫秒數。
設置Timestamp
數據庫裏面存的都是 Timestamp,一般建議存取都用 Timestamp。
設置當前的時間爲 Timestamp:
pst.setTimestamp(5, new java.sql.Timestamp(System.currentTimeMillis()));
通過 Date類 來創建 Timestamp:
java.utl.Date date = new java.util.Date();
pst.setTimestamp(5, new java.sql.Timestamp(date.getTime()));
事實上,一般這裏直接用字符串也能設置,但對於數據庫存在性能問題,一般不建議這樣做:
pst.setString(5, "2020-06-27 00:00:00");
取出Timestamp
因爲從數據庫直接取出的是 Timestamp,需要使用 SimpleDateFormat 來格式化,才能打印出我們可以識別的時間字符串。
Timestamp timestamp = rs.getTimestamp(5);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(timestamp));
事務控制
爲什麼需要事務
一個事務是一系列用戶定義的數據庫操作序列(CRUD)。它的目的是把數據庫的多個操作合併抽象成一個操作。
事務的設計哲學: 要麼都成功,要麼都失敗
。這就是事務的原子性。
事務是隔離的,併發執行的事務之間不互相干擾。
如何實現
事務的實現靠的是回滾機制。
當你做完一個操作的時候,都有日誌文件記錄下你修改的數據。如果你接下來的操作出現了問題,那麼數據庫就能根據日誌文件,運行逆操作,回到原來的狀態。
JDBC中的事務編程
可以在最開始使用 setAutoCommit(false)
來關閉自動提交。
所謂的自動提交就是,mysql 的 JDBC實現 默認是一旦運行了 execute
相關的那三個函數,就會自動運行 commit()
函數,以更新數據庫。
然後在 try 語句的最後使用 commit()
提交。
最後不要忘記,異常處理,如果發生了異常,就要使用 rollback()
函數回滾,使前面的操作全部無效。
我們來看一個結合來前面所有知識的例子:
import java.sql.SQLException;
public class Test {
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws SQLException {
java.sql.Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/booklib", "root",
"123456");
try {
conn.setAutoCommit(false);
String sql = "select * from BeanBook";
java.sql.PreparedStatement pst = conn.prepareStatement(sql);
java.sql.ResultSet rs = pst.executeQuery();
while (rs.next()) {
System.out.println(rs.getString(1) + "\t" + rs.getString(2) + "\t" + rs.getDouble(4));
}
sql = "delete from BeanBook where price>50";
pst = conn.prepareStatement(sql);
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null)
try {
conn.rollback();
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
mysql 中的存儲引擎有:
InnoDB
、MyISAM
等等,但是事務控制只有 InnoDB 支持。
連接池
連接池的概念
之前每次我們使用數據庫 CRUD 的時候,我們每次都需要新建一個連接 Connection。
創建連接和關閉連接的過程也是比較消耗時間的,當線程數量很大的時候,系統就會變得卡頓。
連接池就是爲了解決這個問題。連接池的設計哲學是: 總是借,而不創建
。
我們在一開始先創建一定數量的連接 Connection,然後每次有請求連接的時候,就找空閒的連接分配過去。如果沒有空閒,則需要等待。
實現連接池
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class ConnectionPool {
int size;
List<Connection> conns = new ArrayList<Connection>();
public ConnectionPool(int size) {
//構造器
this.size = size;
init();
}
public void init() {
//初始化連接池
try {
// Class.forName("com.mysql.jdbc.Driver"); //mysql5
Class.forName("com.mysql.cj.jdbc.Driver"); // mysql6+
for (int i = 0; i < size; i++) {
Connection conn = DriverManager
.getConnection("jdbc:mysql://127.0.0.1:3306/booklib", "root", "123456");
conns.add(conn);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public synchronized Connection getConnection() {
//獲得一個連接
while (conns.isEmpty()) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Connection conn = conns.remove(0);
return conn;
}
public synchronized void returnConnection(Connection conn) {
//返還一個連接
conns.add(conn);
this.notifyAll();
}
}
使用開源連接池(以 C3P0 爲例)
一些著名的開源連接池
- DBCP
- C3P0
C3P0連接池
的使用:
import java.beans.PropertyVetoException;
import java.sql.Connection;
import java.sql.SQLException;
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class ConnectionPoo {
private static final String url="jdbc:mysql://localhost:3306/booklib";
private static final String user="root";
private static final String password="123456";
private static ComboPooledDataSource dataSource;
static{
try {
dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(password);
dataSource.setJdbcUrl(url);
//dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
dataSource.setInitialPoolSize(5);
dataSource.setMinPoolSize(1);
dataSource.setMaxPoolSize(10);
dataSource.setMaxStatements(50);
dataSource.setMaxIdleTime(60);
} catch (PropertyVetoException e) {
throw new RuntimeException(e);
}
}
public static Connection getConnection() throws SQLException{
return dataSource.getConnection();
}
}
c3p0 v0.9.2版本 之後,從中分離了一個
mchange-commons-java
包,作爲使用 c3p0 的輔助包。我們這裏調用的就是輔助包。
OR映射
JavaBean
一個 JavaBean 對象需要滿足的條件:
- 提供一個默認的無參構造函數。
- 需要被序列化並且實現 Serializable 接口。
- 一系列可讀寫屬性。
- 一系列的 getter 或 setter 方法。
所有對 JavaBean 屬性的訪問都應當使用 getter 和 setter 方法。
JavaBean 是一個可複用的組件,把應用的業務邏輯和顯示邏輯分離開,降低了開發的複雜程度和維護成本。
POJO (Plain Ordinary Java Object)
POJO 是純粹的 JavaBean。
JavaBean除了滿足上面的條件,沒有規定你不能定義其他東西,就算你把一些業務的代碼加入進來也沒關係。
POJO 不允許有業務方法,也不能攜帶 Connection 之類的方法。
一個簡單的 POJO 對象:
public class BeanPublisher {
private String pubid;
private String publisherName;
private String address;
public String getPubid() {
return pubid;
}
public void setPubid(String pubid) {
this.pubid = pubid;
}
public String getPublisherName() {
return publisherName;
}
public void setPublisherName(String publisherName) {
this.publisherName = publisherName;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
EntityBean 與 OR映射
OR映射 是把數據庫對象和編程語言中的對象映射在一起,他們擁有一樣的屬性。
EntityBean
一般用於ORM對象關係映射,一個實體映射成一張表。
它能執行很多自動化操作:
- 創建一個
EntityBean
對象相當於創建一條記錄 - 刪除一個
EntityBean
對象會同時從數據庫中刪除對應記錄 - 修改一個
EntityBean
時,容器會自動將EntityBean
的狀態和數據庫同步
一些流行的 ORM 框架:
- Hibernate
- MyBati