JDBC的基本使用2
1、maven項目使用JDBC
新建一個 maven JavaSE 項目,往 pom.xml 文件中添加以下依賴:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.12</version> </dependency>
依賴添加完成后就可以直接使用 JDBC 連接數(shù)據(jù)庫并操作了:
public class JdbcTest01 { public static void main(String[] args) { Connection conn = null; Statement stmt = null; try { //1.注冊驅(qū)動(mysql5之后的驅(qū)動jar包可以省略注冊驅(qū)動的步驟) Class.forName("com.mysql.jdbc.Driver"); //2.獲取數(shù)據(jù)庫連接對象 conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456"); //3.定義sql語句 String sql = "update student set name = 'hahaha' where id = 1"; //4.獲取執(zhí)行sql的對象 stmt = conn.createStatement(); //5.執(zhí)行sql int count = stmt.executeUpdate(sql); System.out.println(count); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } finally { //6.釋放資源。為了避免空指針異常,必須先判斷是否為null if(stmt != null) { try { stmt.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(conn != null) { try { conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } }
上面執(zhí)行完成后輸出結(jié)果為 1 。
2、Statement 對象實現(xiàn)增刪改查
2.1、statement對象基本介紹
我們可以通過 Connection 對象的 createStatement() 方法來創(chuàng)建一個 Statement 對象,以此來執(zhí)行 SQL 語句:
Connection conn = DriverManager.getConnection(URL, 用戶名, 密碼); //建立連接 Statement stmt = conn.createStatement(); //創(chuàng)建statement對象 stmt.execute(SqlStr); //執(zhí)行SQL
當(dāng)你創(chuàng)建了一個 Statement 對象之后,你可以用它的三個執(zhí)行方法的任一方法來執(zhí)行 SQL 語句。
-
boolean execute(String SQL) : 可以執(zhí)行任何SQL語句,但是這個語句較少使用。如果第一個結(jié)果是一個 ResultSet 對象,則返回的布爾值為 true ,否則都會返回 false 。當(dāng)你需要使用真正的動態(tài) SQL 時,可以使用這個方法來執(zhí)行 SQL DDL 語句。
-
int executeUpdate(String SQL) : 常用該方法來執(zhí)行DML語句(增刪改),也可執(zhí)行DDL語句(操作數(shù)據(jù)庫和表結(jié)構(gòu))。它返回的是執(zhí)行 SQL 語句影響的行的數(shù)目。
- ResultSet executeQuery(String SQL) : 常用該方法執(zhí)行DQL語句(查詢),它返回一個 ResultSet 對象。
在使用后 statement 對象后我們應(yīng)該關(guān)閉它,通過調(diào)用 close() 方法就可以關(guān)閉。其實在我們關(guān)閉了 Connection 對象后,它也會自動關(guān)閉 Statement 對象。但我們應(yīng)該始終明確關(guān)閉 Statement 對象,以確保真正的清除。
Statement stmt = null; try { stmt = conn.createStatement( ); ... } catch (SQLException e) { ... } finally { stmt.close(); }
2.2、statement對象實現(xiàn)DML(增刪改)
DML(增刪改)語句都可以使用 execute() 或者 executeUpdate() 方法,一般我們可以使用 executeUpdate() 方法,因為該方法能返回 SQL 語句影響的行的數(shù)目。
-
boolean execute(String SQL) : 該方法可以執(zhí)行任何的SQL語句。如果第一個結(jié)果是一個 ResultSet 對象,則返回的布爾值為 true ,否則返回 false 。當(dāng)你需要使用真正的動態(tài) SQL 時,可以使用這個方法來執(zhí)行 SQL DDL 語句。
-
int executeUpdate(String SQL) : 常用該方法來執(zhí)行DML語句(增刪改),也可執(zhí)行DDL語句(操作數(shù)據(jù)庫和表結(jié)構(gòu))。它返回的是執(zhí)行 SQL 語句影響的行的數(shù)目。
增刪改語句執(zhí)行方式都一樣,只是SQL不一樣而已:
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
String insertSql = "insert into student values (null, 'aaa', 22)"; //添加語句,自增主鍵可以賦值為null,數(shù)據(jù)庫會自己處理
String delSql = "delete from student where name = 'aaa'"; //刪除語句
String updateSql = "update student set name = 'newName' where name = 'aaaa'"; //修改語句
Statement stmt = conn.createStatement();
int count = stmt.executeUpdate(updateSql); //增刪改只需直接替換sql語句就行,執(zhí)行方式都一樣
if(count > 0) {
System.out.println("成功" + count);
}else {
System.out.println("失敗" + count);
}
2.3、statement對象實現(xiàn)DDL(操作數(shù)據(jù)庫和表)
DDL 語句也同樣可以使用 executeUpdate() 方法:
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
String insertSql = "alter table student add score varchar(20)"; //增加一列
Statement stmt = conn.createStatement();
int count = stmt.executeUpdate(insertSql);
executeUpdate() 執(zhí)行 DDL 語句時返回值為 0
2.4、statement對象實現(xiàn)DQL(查詢)
查詢語句應(yīng)該使用 executeQuery() 方法,該方法返回一個 ResultSet 對象,我們可以通過遍歷該對象來獲取到查詢到的結(jié)果集:
public void selectTest() { Connection conn = null; Statement stmt = null; ResultSet resultSet = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456"); String selectSql = "select * from student"; stmt = conn.createStatement(); resultSet = stmt.executeQuery(selectSql); while (resultSet.next()){ //循環(huán)判斷此時是否還有數(shù)據(jù) int id = resultSet.getInt("id"); String username = resultSet.getString("name"); int age = resultSet.getInt("age"); System.out.println(id + "," + username + "," + age); } } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } finally { //釋放資源,最后用的最先釋放 if (resultSet != null){ try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if(stmt != null) { try { stmt.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(conn != null) { try { conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
ResultSet 資源也應(yīng)該要主動釋放。
2.4.1、ResultSet 對象
ResultSet 結(jié)果集對象,用于封裝查詢結(jié)果。
該對象的常用方法:
- boolean resultsetObj.next():游標(biāo)向下移動一行,并且判斷當(dāng)前行是否是最后一行的末尾,即是否還有數(shù)據(jù),有則返回 true,否則返回 false。游標(biāo)在開始時指向的是查詢到的數(shù)據(jù)集的第一行的上一行,也就是開始時就必須使用 next() 才有數(shù)據(jù)。
- getXxx(參數(shù)):獲取數(shù)據(jù)。參數(shù)可以是列的編號(從1開始),比如 getString(1),或者是列的名稱,如 getString("username")。
游標(biāo),類似指針?biāo)饕?梢岳斫鉃椋畛跻婚_始時游標(biāo)指在“列名”上,要取到數(shù)據(jù)就需要讓游標(biāo)向下移動,移動后就指向了第一行數(shù)據(jù),然后就可以把第一行的每一列都取出來,一次只能獲取一行中的一列數(shù)據(jù)。

3、PreparedStatement對象實現(xiàn)增刪改查
PreparedStatement 接口擴展了 Statement 接口,它讓你用一個常用的 Statement 對象增加幾個高級功能,這個 statement 對象可以提供靈活多變的動態(tài)參數(shù)。
創(chuàng)建 PreparedStatement 對象:
String sqlStr = "Update Employees SET age = ? WHERE id = ?"; PreparedStatement pstmt = conn.prepareStatement(sqlStr);
JDBC 中所有的參數(shù)都被用 ? 符號表示,這是已知的參數(shù)標(biāo)記。在執(zhí)行 SQL 語句之前,你必須賦予每一個參數(shù)確切的數(shù)值。
setXXX() 方法將值綁定到參數(shù),其中 XXX 表示你希望綁定到輸入?yún)?shù)的 Java 數(shù)據(jù)類型。如果你忘了賦予值,你將收到一個 SQLException。每個參數(shù)標(biāo)記映射它的序號位置,第一標(biāo)記表示位置 1 ,下一個位置為 2 等等。
String sql = "SELECT * FROM user WHERE login=? AND pass=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setObject(1, name); ps.setObject(2, pass);
所有的 Statement 對象的方法都與數(shù)據(jù)庫交互,execute()、executeQuery()、及executeUpdate() 也能被 PreparedStatement 對象引用。然而,這些方法被 SQL 語句修改后是可以輸入?yún)?shù)的。
PreparedStatement可以有效防止sql注入,所以生產(chǎn)環(huán)境上一定要使用PreparedStatement,而不能使用Statement。
3.1、PreparedStatement對象實現(xiàn)增刪改
使用 PreparedStatement 對象來執(zhí)行 SQL 跟使用 statement 對象的方式差不多。
@Test public void jdbcTest() { System.out.println(1111); Connection conn = null; PreparedStatement ps = null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_test", "root", "123456"); //改 //String updatesql = "update students set name = ? where id = ?"; //ps = conn.prepareStatement(updatesql); //ps.setObject(1, "newName"); //ps.setInt(2, 1); //增 //String insertSql = "insert into students2 values (?, ?)"; //ps = conn.prepareStatement(insertSql); //ps.setInt(1, 3); //ps.setString(2, "lisi"); //刪 String deleteSql = "delete from students2 where id = ?"; ps = conn.prepareStatement(deleteSql); ps.setInt(1, 3); //執(zhí)行sql。注意,這里無需傳遞sql作為參數(shù) int count = ps.executeUpdate(); System.out.println(count); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } finally { //6.釋放資源。為了避免空指針異常,必須先判斷是否為null if(ps != null) { try { ps.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(conn != null) { try { conn.close(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
PreparedStatement 對象在使用后也需要關(guān)閉,只需簡單調(diào)用 close() 方法就可以完成這項工作。如果你關(guān)閉了 Connection 對象,那么它也會關(guān)閉 PreparedStatement 對象。然而,你應(yīng)該始終明確關(guān)閉 PreparedStatement 對象,以確保真正的清除。
4、SQL注入問題
使用Statement拼字符串非常容易引發(fā)SQL注入的問題,這是因為SQL參數(shù)往往是從方法參數(shù)傳入的。
假設(shè)用戶登錄的驗證方法如下:
User login(String name, String pass) {
...
stmt.executeQuery("SELECT * FROM user WHERE login='" + name + "' AND pass='" + pass + "'");
...
}
其中,參數(shù)name和pass通常都是Web頁面輸入后由程序接收到的。
如果用戶的輸入是程序期待的值,就可以拼出正確的SQL。例如:name = "bob",pass = "1234":
SELECT * FROM user WHERE login='bob' AND pass='1234'
但是,如果用戶的輸入是一個精心構(gòu)造的字符串,就可以拼出意想不到的SQL,這個SQL也是正確的,但它查詢的條件不是程序設(shè)計的意圖。例如:name = "bob' OR pass=", pass = " OR pass='":
SELECT * FROM user WHERE login='bob' OR pass=' AND pass=' OR pass=''
上面的SQL語句執(zhí)行的時候,根本就不用判斷密碼是否正確,這樣一來,登錄就形同虛設(shè)。
要避免SQL注入攻擊,一個辦法是針對所有字符串參數(shù)進行轉(zhuǎn)義,但是轉(zhuǎn)義很麻煩,而且需要在任何使用SQL的地方增加轉(zhuǎn)義代碼。
還有一個辦法就是使用PreparedStatement。使用PreparedStatement可以完全避免SQL注入的問題,因為PreparedStatement始終使用?作為占位符,并且把數(shù)據(jù)連同SQL本身傳給數(shù)據(jù)庫,這樣可以保證每次傳給數(shù)據(jù)庫的SQL語句是相同的,只是占位符的數(shù)據(jù)不同,還能高效利用數(shù)據(jù)庫本身對查詢的緩存。上述登錄SQL如果用PreparedStatement可以改寫如下:
User login(String name, String pass) {
...
String sql = "SELECT * FROM user WHERE login=? AND pass=?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setObject(1, name);
ps.setObject(2, pass);
...
}
所以,PreparedStatement比Statement更安全,而且更快。
注意:使用Java對數(shù)據(jù)庫進行操作時,必須使用PreparedStatement,嚴(yán)禁任何通過參數(shù)拼字符串的代碼!
把上面使用Statement的代碼改為使用PreparedStatement,如下:
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) {
ps.setObject(1, "M"); // 注意:索引從1開始
ps.setObject(2, 3);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
long grade = rs.getLong("grade");
String name = rs.getString("name");
String gender = rs.getString("gender");
}
}
}
}
使用PreparedStatement和Statement稍有不同,必須首先調(diào)用setObject()設(shè)置每個占位符?的值,最后獲取的仍然是ResultSet對象。另外注意到從結(jié)果集讀取列時,使用String類型的列名比索引要易讀,而且不易出錯。
注意到JDBC查詢的返回值總是ResultSet,即使我們寫這樣的聚合查詢SELECT SUM(score) FROM ...,也需要按結(jié)果集讀取:
ResultSet rs = ...
if (rs.next()) {
double sum = rs.getDouble(1);
}
5、mysql數(shù)據(jù)類型和java數(shù)據(jù)類型映射關(guān)系
使用JDBC的時候,我們需要在SQL數(shù)據(jù)類型和Java數(shù)據(jù)類型之間進行轉(zhuǎn)換。JDBC在java.sql.Types定義了一組常量來表示如何映射SQL數(shù)據(jù)類型,但是平時我們使用的類型通常也就以下幾種:

只有最新的JDBC驅(qū)動才支持LocalDate和LocalTime。
詳細表格:

6、流式查詢
在 Java 中進行數(shù)據(jù)庫數(shù)據(jù)的流式讀取,意味著從數(shù)據(jù)庫中逐行獲取數(shù)據(jù),每次只在內(nèi)存中保留當(dāng)前正在處理的一行數(shù)據(jù),減少內(nèi)存占用,而不是一次性將所有數(shù)據(jù)加載到內(nèi)存中,這樣可以有效減少內(nèi)存占用,尤其適用于處理大量數(shù)據(jù)的場景。
以 MySQL 為例,在流式讀取數(shù)據(jù)時,需要將PreparedStatement 的 ResultSet 獲取方式設(shè)置為 ResultSet.TYPE_FORWARD_ONLY 和 ResultSet.CONCUR_READ_ONLY,并將 fetchSize 設(shè)置為 Integer.MIN_VALUE(對于 MySQL 數(shù)據(jù)庫),以啟用流式讀取:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; public class MysqlStreamQueryWithPreparedStatement { public static void main(String[] args) { // 數(shù)據(jù)庫連接信息 String url = "jdbc:mysql://localhost:3306/your_database"; String username = "your_username"; String password = "your_password"; // 定義 SQL 查詢語句 String sql = "SELECT * FROM your_table WHERE some_column = ?"; try (Connection connection = DriverManager.getConnection(url, username, password); // 創(chuàng)建 PreparedStatement 對象,設(shè)置結(jié)果集類型和并發(fā)模式 PreparedStatement preparedStatement = connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { // 設(shè)置流式查詢的 fetchSize preparedStatement.setFetchSize(Integer.MIN_VALUE); // 設(shè)置 SQL 參數(shù) preparedStatement.setString(1, "your_condition_value"); // 執(zhí)行查詢,獲取結(jié)果集 try (ResultSet resultSet = preparedStatement.executeQuery()) { // 遍歷結(jié)果集,此時每次只會加載一行數(shù)據(jù)到內(nèi)存 while (resultSet.next()) { // 從結(jié)果集中獲取數(shù)據(jù) int id = resultSet.getInt("id"); String name = resultSet.getString("name"); System.out.println("ID: " + id + ", Name: " + name); } } } catch (Exception e) { // 打印異常信息 e.printStackTrace(); } } }
ResultSet.TYPE_FORWARD_ONLY
用于指定 ResultSet.TYPE_FORWARD_ONLYResultSet 對象的游標(biāo)(Cursor)只能向前移動,也就是只能按順序從第一行開始依次向后遍歷結(jié)果集,不能向后滾動到之前已經(jīng)訪問過的行。
流式查詢的核心目標(biāo)是逐行處理數(shù)據(jù),避免一次性將大量數(shù)據(jù)加載到內(nèi)存中。ResultSet.TYPE_FORWARD_ONLY 規(guī)定結(jié)果集的游標(biāo)只能向前移動,數(shù)據(jù)庫驅(qū)動程序無需為游標(biāo)回滾保存之前讀取的數(shù)據(jù)。這樣一來,內(nèi)存中僅需保留當(dāng)前正在處理的行數(shù)據(jù),大大降低了內(nèi)存使用量。特別是在處理大規(guī)模數(shù)據(jù)集時,這種內(nèi)存優(yōu)化至關(guān)重要,可有效防止內(nèi)存溢出錯誤。
ResultSet.CONCUR_READ_ONLY
ResultSet.CONCUR_READ_ONLY 用于指定 ResultSet 對象是只讀的,即不能通過 ResultSet 對象對數(shù)據(jù)庫中的數(shù)據(jù)進行更新、插入或刪除操作。這簡化了數(shù)據(jù)庫驅(qū)動程序的實現(xiàn),因為它無需處理復(fù)雜的并發(fā)控制和數(shù)據(jù)修改邏輯。對于流式查詢,通常只關(guān)注數(shù)據(jù)的讀取和處理,不涉及對數(shù)據(jù)的修改,所以只讀模式可以避免不必要的復(fù)雜性。
- fetchSize
流式查詢需要將 fetchSize 設(shè)置為 Integer.MIN_VALUE(對于 MySQL 數(shù)據(jù)庫),以啟用流式查詢。這樣可以確保每次只從數(shù)據(jù)庫獲取一行數(shù)據(jù)。非流式查詢一般不需要顯式設(shè)置 fetchSize,數(shù)據(jù)庫驅(qū)動會使用默認的獲取大小,可能會一次性獲取較多的數(shù)據(jù)到內(nèi)存中。
mybatis 實現(xiàn)流式查詢可參考:https://blog.csdn.net/weixin_52100990/article/details/137210798
6.1、流式查詢和非流式查詢的代碼區(qū)別
非流式查詢是 JDBC 的默認方式,以 MySQL 為示例代碼如下:
String sql = "SELECT * FROM large_table WHERE id > ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, 1000); // 設(shè)置參數(shù) try (ResultSet rs = pstmt.executeQuery()) { // 所有數(shù)據(jù)已加載到內(nèi)存 while (rs.next()) { // 處理一行數(shù)據(jù)(但所有數(shù)據(jù)已加載到內(nèi)存!) System.out.println(rs.getInt("id")); } } }
6.2、流式查詢和非流式查詢對比
| 特性 | 流式查詢 | 非流式查詢 |
|---|---|---|
| 數(shù)據(jù)加載方式 | 逐行從數(shù)據(jù)庫讀取(按需加載) | 一次性加載所有結(jié)果到內(nèi)存 |
| 內(nèi)存占用 | 恒定(低內(nèi)存消耗) | 隨結(jié)果集增大線性增長(高內(nèi)存消耗) |
| 適用場景 | 大數(shù)據(jù)量(如百萬級記錄) | 小數(shù)據(jù)量 |
| 數(shù)據(jù)庫連接占用時間 | 較長(需保持連接直到結(jié)果集遍歷完成) | 較短(數(shù)據(jù)加載后即可關(guān)閉) |
| 默認行為 | 需顯式配置 | JDBC 默認方式 |
| 使用場景 | 大結(jié)果集(> 1萬行) | 小結(jié)果集(< 1萬行) |
非流式查詢:
[APP] <--- 一次性加載 100萬行 --- [DB Server]
(內(nèi)存爆滿!)
流式查詢:
[APP] <--- 逐行/分批傳輸 --- [DB Server]
(內(nèi)存恒定,按需加載)
非流式查詢一般只適用于小數(shù)據(jù)量查詢,如果涉及大數(shù)據(jù)量,為避免內(nèi)存溢出、性能等問題,建議使用流式查詢。
流式查詢會長時間占用數(shù)據(jù)庫連接,需確保連接池配置合理,如增加超時時間避免超時。不同數(shù)據(jù)庫使用流式查詢的代碼可能也不一致。

浙公網(wǎng)安備 33010602011771號