PreparedStatement 参数化查询
本文你会学到 :
PreparedStatement 相比 Statement 的两大优势(防注入 + 预编译)
参数化查询、插入、更新的标准写法
同一 PreparedStatement 对象的多次复用
BLOB 大字段的读写方法
💡 概念与优势
为什么要用 PreparedStatement?
防 SQL 注入:参数通过占位符 ? 绑定,驱动对参数值进行转义,彻底防止注入攻击
预编译性能:SQL 结构只解析、编译一次,多次执行只需替换参数值,减少数据库解析开销
🔐 参数化 CRUD 操作
参数化查询
PreparedStatement 带 ? 占位符查询 /**
* 演示 PreparedStatement 带 ? 占位符查询
*/
@Test
void testParameterizedQuery () throws SQLException {
// 使用 ? 作为参数占位符,避免 SQL 注入
String sql = "SELECT * FROM employees WHERE department = ?" ;
try ( PreparedStatement pstmt = conn . prepareStatement ( sql )) {
// 绑定参数(索引从 1 开始)
pstmt . setString ( 1 , "技术部" );
try ( ResultSet rs = pstmt . executeQuery ()) {
int count = 0 ;
while ( rs . next ()) {
System . out . printf ( "技术部员工: id=%d, name=%s, salary=%.1f%n" ,
rs . getInt ( "id" ),
rs . getString ( "name" ),
rs . getDouble ( "salary" ));
count ++ ;
}
assertEquals ( 2 , count , "技术部应有 2 名员工" );
}
}
}
参数化插入
PreparedStatement 参数化插入,绑定多种类型参数 /**
* 演示 PreparedStatement 插入数据,绑定多种类型的参数
*/
@Test
void testParameterizedInsert () throws SQLException {
String sql = "INSERT INTO employees (name, department, salary) VALUES (?, ?, ?)" ;
try ( PreparedStatement pstmt = conn . prepareStatement ( sql )) {
// 分别使用 setString、setString、setDouble 绑定不同类型参数
pstmt . setString ( 1 , "孙七" );
pstmt . setString ( 2 , "财务部" );
pstmt . setDouble ( 3 , 14000.0 );
int rowsAffected = pstmt . executeUpdate ();
assertEquals ( 1 , rowsAffected , "插入应影响 1 行" );
System . out . println ( "PreparedStatement 插入影响行数: " + rowsAffected );
// 验证插入成功
try ( Statement stmt = conn . createStatement ();
ResultSet rs = stmt . executeQuery ( "SELECT COUNT(*) FROM employees" )) {
rs . next ();
assertEquals ( 5 , rs . getInt ( 1 ), "插入后应有 5 条记录" );
}
}
}
参数化更新
PreparedStatement 参数化 UPDATE /**
* 演示 PreparedStatement 更新操作
*/
@Test
void testParameterizedUpdate () throws SQLException {
String sql = "UPDATE employees SET salary = ? WHERE name = ?" ;
try ( PreparedStatement pstmt = conn . prepareStatement ( sql )) {
pstmt . setDouble ( 1 , 20000.0 ); // 新薪资
pstmt . setString ( 2 , "张三" ); // 更新条件
int rowsAffected = pstmt . executeUpdate ();
assertEquals ( 1 , rowsAffected , "更新应影响 1 行" );
// 验证更新成功
try ( PreparedStatement query = conn . prepareStatement (
"SELECT salary FROM employees WHERE name = ?" )) {
query . setString ( 1 , "张三" );
try ( ResultSet rs = query . executeQuery ()) {
assertTrue ( rs . next ());
assertEquals ( 20000.0 , rs . getDouble ( "salary" ), 0.01 ,
"张三的薪资应更新为 20000.0" );
System . out . println ( "张三薪资更新后: " + rs . getDouble ( "salary" ));
}
}
}
}
防 SQL 注入对比验证
Statement 注入成功 vs PreparedStatement 注入失败 /**
* 对比 Statement(注入成功)和 PreparedStatement(注入失败)
* 用断言证明 PreparedStatement 的安全性
*/
@Test
void testPreventSqlInjection () throws SQLException {
String maliciousInput = "' OR '1'='1" ;
// --- Statement:SQL 注入成功 ---
try ( Statement stmt = conn . createStatement ()) {
String unsafeSql = "SELECT * FROM employees WHERE name = '" + maliciousInput + "'" ;
ResultSet rs = stmt . executeQuery ( unsafeSql );
int unsafeCount = 0 ;
while ( rs . next ()) unsafeCount ++ ;
// 注入成功:查到了所有 4 条记录
assertEquals ( 4 , unsafeCount , "Statement 拼接导致 SQL 注入,查到所有记录" );
System . out . println ( "⚠️ Statement 拼接(注入成功): 查到 " + unsafeCount + " 条" );
}
// --- PreparedStatement:SQL 注入失败 ---
String safeSql = "SELECT * FROM employees WHERE name = ?" ;
try ( PreparedStatement pstmt = conn . prepareStatement ( safeSql )) {
// 恶意输入被当作普通字符串字面值处理,不会破坏 SQL 结构
pstmt . setString ( 1 , maliciousInput );
try ( ResultSet rs = pstmt . executeQuery ()) {
int safeCount = 0 ;
while ( rs . next ()) safeCount ++ ;
// 注入失败:没有名字叫 "' OR '1'='1" 的员工,查到 0 条
assertEquals ( 0 , safeCount ,
"PreparedStatement 防止了 SQL 注入,恶意输入被当作字面值" );
System . out . println ( "✅ PreparedStatement(注入失败): 查到 " + safeCount + " 条" );
}
}
}
⚡ 预编译复用与大字段
同一条 SQL 要执行多次怎么办?——预编译复用
同一个 PreparedStatement 对象可通过重新绑定参数多次执行,SQL 只编译一次。
复用同一 PreparedStatement 对象执行多次查询 /**
* 演示复用同一个 PreparedStatement 对象多次执行
* 预编译的 SQL 只解析一次,改变参数值即可重复执行,性能更优
*/
@Test
void testReusePreparedStatement () throws SQLException {
String sql = "SELECT * FROM employees WHERE department = ?" ;
try ( PreparedStatement pstmt = conn . prepareStatement ( sql )) {
// 第一次执行:查询技术部
pstmt . setString ( 1 , "技术部" );
try ( ResultSet rs = pstmt . executeQuery ()) {
int count = 0 ;
while ( rs . next ()) count ++ ;
assertEquals ( 2 , count , "技术部应有 2 名员工" );
System . out . println ( "第1次执行(技术部): " + count + " 条" );
}
// 第二次执行:改变参数值,查询市场部(复用同一个 PreparedStatement)
pstmt . setString ( 1 , "市场部" );
try ( ResultSet rs = pstmt . executeQuery ()) {
int count = 0 ;
while ( rs . next ()) count ++ ;
assertEquals ( 2 , count , "市场部应有 2 名员工" );
System . out . println ( "第2次执行(市场部): " + count + " 条" );
}
// 第三次执行:查询不存在的部门
pstmt . setString ( 1 , "不存在的部门" );
try ( ResultSet rs = pstmt . executeQuery ()) {
int count = 0 ;
while ( rs . next ()) count ++ ;
assertEquals ( 0 , count , "不存在的部门应返回 0 条记录" );
System . out . println ( "第3次执行(不存在的部门): " + count + " 条" );
}
// 预编译优势:SQL 只编译一次,三次执行只需替换参数值
}
}
怎样在数据库存储文件?——Blob 大字段读写
PreparedStatement 通过 setBytes() / setBinaryStream() 写入二进制大对象(BLOB),通过 getBytes() / getBinaryStream() 读取。典型场景:存储图片、文档、音频等文件数据。
方法
适用场景
setBytes(int, byte[])
小文件,数据已在内存中(推荐)
setBinaryStream(int, InputStream)
大文件,流式写入,不占用内存
getBytes(String)
读取小文件,直接返回 byte[]
getBinaryStream(String)
读取大文件,返回 InputStream
PreparedStatement 写入和读取 BLOB 大字段 /**
* 演示 PreparedStatement 处理 BLOB 大字段
* 写入:setBytes() / setBinaryStream()
* 读取:getBytes() / getBinaryStream()
* 典型场景:存储图片、文件、音频等二进制数据
*/
@Test
void testBlobWriteAndRead () throws Exception {
// 建一张含 BLOB 列的表(在测试内自行建表,避免影响其他测试)
try ( Statement stmt = conn . createStatement ()) {
stmt . execute ( "CREATE TABLE IF NOT EXISTS attachments ("
+ "id INT AUTO_INCREMENT PRIMARY KEY, "
+ "filename VARCHAR(100), "
+ "content BLOB)" );
}
// === 写入 BLOB 数据 ===
byte [] originalData = "Hello, JDBC Blob! 这是二进制内容。" . getBytes ( java . nio . charset . StandardCharsets . UTF_8 );
String insertSql = "INSERT INTO attachments (filename, content) VALUES (?, ?)" ;
try {
try ( PreparedStatement pstmt = conn . prepareStatement ( insertSql )) {
pstmt . setString ( 1 , "test.txt" );
// setBytes:直接传入 byte[](适合小文件)
pstmt . setBytes ( 2 , originalData );
int rows = pstmt . executeUpdate ();
assertEquals ( 1 , rows , "BLOB 写入应影响 1 行" );
System . out . println ( "BLOB 写入成功,数据长度: " + originalData . length + " 字节" );
}
// === 读取 BLOB 数据 ===
String querySql = "SELECT filename, content FROM attachments WHERE filename = ?" ;
try ( PreparedStatement pstmt = conn . prepareStatement ( querySql )) {
pstmt . setString ( 1 , "test.txt" );
try ( ResultSet rs = pstmt . executeQuery ()) {
assertTrue ( rs . next (), "应查到 BLOB 记录" );
String filename = rs . getString ( "filename" );
// getBytes:直接读出 byte[]
byte [] readData = rs . getBytes ( "content" );
assertEquals ( "test.txt" , filename );
assertArrayEquals ( originalData , readData , "读取的 BLOB 数据应与写入的完全一致" );
String readText = new String ( readData , java . nio . charset . StandardCharsets . UTF_8 );
System . out . println ( "BLOB 读取成功,文件名: " + filename + ",内容: " + readText );
}
}
} finally {
// 使用 finally 确保断言失败时也能清理临时表
try ( Statement stmt = conn . createStatement ()) {
stmt . execute ( "DROP TABLE IF EXISTS attachments" );
}
}
}
BLOB 大小限制
MySQL 默认 max_allowed_packet 为 64 MB,存储超大文件建议使用对象存储(OSS/S3),数据库只存元数据和文件路径。
优先使用 PreparedStatement
任何含用户输入的 SQL 都应使用 PreparedStatement。即使是内部系统无注入风险的场景,预编译带来的性能提升和代码可读性提升也值得优先选择 PreparedStatement。