一、执行 SQL 语句的三种接口 数据库连接被用于向数据库服务器发送命令和 SQL 语句,并接受数据库服务器返回的结果。一个数据库连接就是一个 Socket 连接。
在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式:
接口
说明
特点
Statement
用于执行静态 SQL 语句
不预编译,存在 SQL 注入风险
PreparedStatement
SQL 语句被预编译并存储
推荐使用 ,可防止 SQL 注入,效率高
CallableStatement
用于执行 SQL 存储过程
调用数据库存储过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ┌─────────────────────────────────────────────┐ │ Statement │ │ executeQuery("SELECT * FROM emp") │ │ executeUpdate("INSERT INTO ...") │ ├─────────────────────────────────────────────┤ │ PreparedStatement │ │ SQL 预编译,使用 ? 占位符 │ │ executeQuery() 无参方法 │ │ 安全性高,可重复执行 │ ├─────────────────────────────────────────────┤ │ CallableStatement │ │ 调用存储过程 │ │ {call procedure_name(?, ?, ?)} │ └─────────────────────────────────────────────┘
1.1 创建 Statement 对象 1 2 Connection conn = DBUtil.getConnection();Statement stmt = conn.createStatement();
1.2 创建 PreparedStatement 对象 1 2 3 4 Connection conn = DBUtil.getConnection();String sql = "SELECT * FROM emp WHERE id = ?" ;PreparedStatement ps = conn.prepareStatement(sql);ps.setInt(1 , 10 );
1.3 创建 CallableStatement 对象 1 2 3 4 5 6 Connection conn = DBUtil.getConnection();String sql = "{call update_salary(?, ?, ?)}" ;CallableStatement cs = conn.prepareCall(sql);cs.setInt(1 , 19 ); cs.registerOutParameter(2 , Types.DECIMAL); cs.registerOutParameter(3 , Types.DECIMAL);
二、Statement 详解 2.1 常用方法
方法
说明
executeQuery(String sql)
执行 SELECT 查询,返回 ResultSet
executeUpdate(String sql)
执行 INSERT/UPDATE/DELETE,返回影响行数
execute(String sql)
执行任意 SQL,返回 boolean
addBatch(String sql)
添加批处理 SQL
executeBatch()
执行批处理
clearBatch()
清空批处理
2.2 Statement 执行 CRUD 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String sql = "SELECT * FROM emp" ;ResultSet rs = stmt.executeQuery(sql);String sql = "INSERT INTO emp(name, salary) VALUES('张三', 8000)" ;int rows = stmt.executeUpdate(sql);System.out.println("插入了 " + rows + " 行" ); String sql = "UPDATE emp SET salary = 9000 WHERE id = 1" ;int rows = stmt.executeUpdate(sql);String sql = "DELETE FROM emp WHERE id = 1" ;int rows = stmt.executeUpdate(sql);
2.3 Statement 的缺点
SQL 注入风险 :用户输入的内容可能被拼接到 SQL 中
性能低 :每次执行都需要编译 SQL
不可重复使用 :每次需要传入完整 SQL
1 2 3 4 5 String name = "admin' OR '1'='1" ;String sql = "SELECT * FROM user WHERE name = '" + name + "' AND password = '123'" ;
三、PreparedStatement 详解 3.1 为什么使用 PreparedStatement
特性
Statement
PreparedStatement
SQL 注入
❌ 存在风险
✅ 可防止
预编译
❌ 每次编译
✅ 预编译一次
性能
低
高(可重复执行)
参数设置
字符串拼接
? 占位符
适用场景
静态 SQL
动态参数 SQL
预编译的优点:
A. 可反复执行 :预编译 SQL 可以被 PreparedStatement 反复执行
B. 性能高 :预编译 SQL 只被数据库解析一次,后续只需传递参数
C. 安全性好 :可以抵御 SQL 注入攻击
D. 占位符 :可以替代表达式中的数据值(不能替代表名、列名)
3.2 PreparedStatement 常用方法
方法
说明
setInt(int index, int value)
设置 int 参数
setString(int index, String value)
设置 String 参数
setDouble(int index, double value)
设置 double 参数
setDate(int index, Date value)
设置 Date 参数
setObject(int index, Object value)
设置 Object 参数
executeQuery()
执行查询(无参)
executeUpdate()
执行更新(无参)
clearParameters()
清除参数
3.3 PreparedStatement 执行 CRUD 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 String sql = "SELECT id, name, salary FROM emp WHERE salary > ?" ;PreparedStatement ps = conn.prepareStatement(sql);ps.setDouble(1 , 5000 ); ResultSet rs = ps.executeQuery();while (rs.next()) { System.out.println(rs.getInt("id" ) + " " + rs.getString("name" )); } String sql = "INSERT INTO emp(name, salary, dept_id) VALUES(?, ?, ?)" ;PreparedStatement ps = conn.prepareStatement(sql);ps.setString(1 , "李四" ); ps.setDouble(2 , 8000 ); ps.setInt(3 , 1 ); int rows = ps.executeUpdate();System.out.println("插入了 " + rows + " 行" ); String sql = "UPDATE emp SET salary = ? WHERE id = ?" ;PreparedStatement ps = conn.prepareStatement(sql);ps.setDouble(1 , 10000 ); ps.setInt(2 , 1 ); int rows = ps.executeUpdate();String sql = "DELETE FROM emp WHERE id = ?" ;PreparedStatement ps = conn.prepareStatement(sql);ps.setInt(1 , 1 ); int rows = ps.executeUpdate();
四、获取自增主键 4.1 应用场景 在实际开发中经常需要同时向多张表中插入数据,表和表之间存在关联关系。某张表的主键列是自动生成的,如何在插入数据时同时获取自动生成的主键?
4.2 Connection 接口 1 2 3 4 5 6 PreparedStatement ps = conn.prepareStatement( sql, Statement.RETURN_GENERATED_KEYS );
4.3 Statement 接口 1 2 3 4 5 ResultSet rs = ps.getGeneratedKeys();if (rs.next()) { int id = rs.getInt(1 ); }
4.4 完整示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public interface IEmpDAO { Integer insertReturnKey (Emp emp) ; } public class EmpDAOImpl implements IEmpDAO { @Override public Integer insertReturnKey (Emp emp) { Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; Integer id = null ; try { conn = DBUtil.getConnection(); String sql = "INSERT INTO emp(name, salary, dept_id) VALUES(?, ?, ?)" ; ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1 , emp.getName()); ps.setDouble(2 , emp.getSalary()); ps.setInt(3 , emp.getDeptId()); ps.executeUpdate(); rs = ps.getGeneratedKeys(); if (rs.next()) { id = rs.getInt(1 ); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.closeResource(rs, ps, conn); } return id; } } @Test public void testInsertReturnKey () { Emp emp = new Emp (); emp.setName("王五" ); emp.setSalary(8500 ); emp.setDeptId(1 ); EmpDAOImpl dao = new EmpDAOImpl (); Integer id = dao.insertReturnKey(emp); System.out.println("新增员工的 ID:" + id); }
五、批量操作 5.1 批处理 API
方法
说明
addBatch()
将参数添加到批处理
executeBatch()
执行批处理,返回每条 SQL 影响的行数
clearBatch()
清空批处理中的参数
5.2 批量插入示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.rd;import com.rd.util.DBUtil;import java.sql.*;import java.util.Arrays;public class BatchExample { public static void main (String[] args) throws SQLException { Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false ); String sql = "insert into tb_goods_temp(name) values(?)" ; PreparedStatement ps = conn.prepareStatement(sql); for (int i = 0 ; i < 100 ; i++) { ps.setString(1 , "商品" + (i + 1 )); ps.addBatch(); } int [] arr = ps.executeBatch(); System.out.println(Arrays.toString(arr)); conn.commit(); DBUtil.closeResource(null , ps, conn); } }
5.3 批处理优化技巧 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int batchSize = 500 ;for (int i = 0 ; i < 10000 ; i++) { ps.setString(1 , "商品" + (i + 1 )); ps.addBatch(); if ((i + 1 ) % batchSize == 0 ) { ps.executeBatch(); ps.clearBatch(); } } ps.executeBatch(); ps.clearBatch();
JDBC URL 中添加批处理优化参数:
1 url =jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true
六、分页查询 6.1 分页 SQL 1 SELECT * FROM 表名 LIMIT (pageNum - 1 ) * pageSize, pageSize
6.2 分页工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.qiufenfen.util;import java.util.List;public class PageUtil <E> { private int pageNum; private int pageSize; private long totalRecords; private int totalPages; private int offset; private boolean hasPrevious; private boolean hasNext; private List<E> data; public PageUtil (int pageNum, int pageSize) { if (pageNum < 1 ) { throw new IllegalArgumentException ("页码必须大于0!" ); } if (pageSize < 1 ) { throw new IllegalArgumentException ("每页数量必须大于0!" ); } this .pageNum = pageNum; this .pageSize = pageSize; } public int getTotalPages () { if (totalRecords <= 0 ) return 0 ; return (int ) Math.ceil((double ) totalRecords / pageSize); } public int getOffset () { return (pageNum - 1 ) * pageSize; } public boolean isHasPrevious () { return pageNum > 1 ; } public boolean isHasNext () { return pageNum < getTotalPages(); } public int getPageNum () { return pageNum; } public int getPageSize () { return pageSize; } public long getTotalRecords () { return totalRecords; } public void setTotalRecords (long totalRecords) { this .totalRecords = totalRecords; } public List<E> getData () { return data; } public void setData (List<E> data) { this .data = data; } }
6.3 分页查询实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public PageUtil<Emp> findByPage (int pageNum, int pageSize) { PageUtil<Emp> page = new PageUtil <>(pageNum, pageSize); Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; List<Emp> list = new ArrayList <>(); try { conn = DBUtil.getConnection(); String countSql = "SELECT COUNT(*) FROM emp" ; ps = conn.prepareStatement(countSql); rs = ps.executeQuery(); if (rs.next()) { page.setTotalRecords(rs.getLong(1 )); } String sql = "SELECT * FROM emp LIMIT ?, ?" ; ps = conn.prepareStatement(sql); ps.setInt(1 , page.getOffset()); ps.setInt(2 , page.getPageSize()); rs = ps.executeQuery(); while (rs.next()) { Emp emp = new Emp (); emp.setId(rs.getInt("id" )); emp.setName(rs.getString("name" )); emp.setSalary(rs.getDouble("salary" )); list.add(emp); } page.setData(list); } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.closeResource(rs, ps, conn); } return page; }
七、CallableStatement 调用存储过程 7.1 创建函数和存储过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 CREATE FUNCTION calculate_bonus(p_emp_id INT ) RETURNS DECIMAL (10 ,2 )READS SQL DATADETERMINISTIC BEGIN DECLARE emp_sal INT ; DECLARE emp_bonus DECIMAL (10 ,2 ); SELECT salary INTO emp_sal FROM emp WHERE id = p_emp_id; IF emp_sal IS NULL THEN RETURN 0.0 ; END IF; SET emp_bonus = emp_sal * 0.1 ; RETURN emp_bonus; END ;SELECT calculate_bonus(19 );CREATE PROCEDURE update_salary( IN emp_id INT , OUT old_salary DECIMAL (10 ,2 ), OUT new_salary DECIMAL (10 ,2 ) ) BEGIN SELECT salary INTO old_salary FROM emp WHERE id = emp_id; UPDATE emp SET salary = salary * 1.1 WHERE id = emp_id; SELECT salary INTO new_salary FROM emp WHERE id = emp_id; END ;CALL update_salary(19 , @x , @y );SELECT @x , @y ;
7.2 JDBC 调用函数 1 2 3 4 5 6 7 8 9 String sql = "SELECT calculate_bonus(?)" ;PreparedStatement ps = conn.prepareStatement(sql);ps.setInt(1 , 19 ); ResultSet rs = ps.executeQuery();if (rs.next()) { double bonus = rs.getDouble(1 ); System.out.println("奖金:" + bonus); }
7.3 JDBC 调用存储过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 String sql = "{call update_salary(?, ?, ?)}" ;CallableStatement cs = conn.prepareCall(sql);cs.setInt(1 , 19 ); cs.registerOutParameter(2 , Types.DECIMAL); cs.registerOutParameter(3 , Types.DECIMAL); cs.execute(); double oldSalary = cs.getDouble(2 );double newSalary = cs.getDouble(3 );System.out.println("旧薪资:" + oldSalary + ",新薪资:" + newSalary);
8.1 ResultSet 结果集 ResultSet 是 SELECT 查询返回的结果集。
1 2 3 4 5 6 7 8 9 10 11 12 ResultSet rs = ps.executeQuery();while (rs.next()) { int deptno = rs.getInt("deptno" ); String dname = rs.getString("dname" ); String loc = rs.getString("loc" ); int id = rs.getInt(1 ); String name = rs.getString(2 ); }
常用 getXXX 方法:
方法
说明
getInt(String columnLabel)
按列名获取 int
getInt(int columnIndex)
按列索引获取 int
getString(String columnLabel)
按列名获取 String
getDouble(String columnLabel)
按列名获取 double
getDate(String columnLabel)
按列名获取 Date
getObject(String columnLabel)
按列名获取 Object
ResultSetMetaData 可获取结果集的元信息(列数、列名、列类型等)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ResultSet rs = ps.executeQuery();ResultSetMetaData metaData = rs.getMetaData();int columnCount = metaData.getColumnCount();for (int i = 1 ; i <= columnCount; i++) { String columnName = metaData.getColumnName(i); String columnLabel = metaData.getColumnLabel(i); String columnTypeName = metaData.getColumnTypeName(i); int columnType = metaData.getColumnType(i); int precision = metaData.getPrecision(i); int scale = metaData.getScale(i); boolean isNullable = metaData.isNullable(i) == ResultSetMetaData.columnNullable; System.out.println("列名:" + columnName + ",类型:" + columnTypeName + ",精度:" + precision); }
8.3 通用查询方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public List<Map<String, Object>> query (String sql, Object... params) { List<Map<String, Object>> list = new ArrayList <>(); Connection conn = null ; PreparedStatement ps = null ; ResultSet rs = null ; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); for (int i = 0 ; i < params.length; i++) { ps.setObject(i + 1 , params[i]); } rs = ps.executeQuery(); ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); while (rs.next()) { Map<String, Object> row = new HashMap <>(); for (int i = 1 ; i <= columnCount; i++) { String columnName = metaData.getColumnLabel(i); Object value = rs.getObject(i); row.put(columnName, value); } list.add(row); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.closeResource(rs, ps, conn); } return list; }
九、JDBC 面试题 9.1 Statement 和 PreparedStatement 的区别
对比项
Statement
PreparedStatement
SQL 注入
有风险
可防止
预编译
不预编译
预编译,可重复执行
性能
每次都编译
编译一次,多次执行
参数传递
字符串拼接
? 占位符 + setXXX
安全性
低
高
9.2 PreparedStatement 为什么能防止 SQL 注入
PreparedStatement 使用预编译 机制,SQL 语句结构在创建时已固定
? 占位符只允许传递数据值 ,传递的内容不会被当作 SQL 语法解析
即使输入中包含 ' OR '1'='1 等注入内容,也只会被当作普通字符串处理
9.3 PreparedStatement 的预编译过程 1 2 3 4 5 1. 创建 PreparedStatement 时,SQL 语句被传递给数据库解析 2. 数据库解析 SQL 结构(语法检查、权限检查、优化执行计划) 3. 后续执行时,只传递占位符参数 4. 批量插入 1000 条记录时,SQL 只被解析一次 5. 其余只需接受参数并执行,速度大为提高
💡 小结 :本章深入讲解了 JDBC 的三种执行 SQL 对象。Statement 适用于静态 SQL,PreparedStatement 是最常用的方式(安全、高效),CallableStatement 用于调用存储过程。重点掌握了 PreparedStatement 的 CRUD 操作、获取自增主键、批量操作、分页查询。最后介绍了 ResultSet 结果集处理和 ResultSetMetaData 元数据的使用。