SQL注入问题解析
本文通过Java JDBC实现用户登录功能,演示了SQL注入问题及其危害。通过对比Statement和PreparedStatement,详细解释了SQL注入的根本原因及如何通过PreparedStatement预编译机制有效避免注入攻击。同时,介绍了PreparedStatement在增删改查、模糊查询、分页查询及批处理中的应用,提升数据库操作效率与安全性
一、实现最基本的登录功能
使用t_user表
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;
public class JDBCTest12 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 初始化登录界面
System.out.println("欢迎使用用户管理系统,请登录!");
System.out.print("用户名:");
String loginName = scanner.next();
System.out.print("密码:");
String loginPwd = scanner.next();
// 连接数据库,验证用户名和密码是否正确。
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
boolean loginSuccess = false;
String realname = null;
try {
// 获取连接
conn = DbUtils.getConnection();
// 获取数据库操作对象
stmt = conn.createStatement();
// 执行SQL语句
//+号是为了拼成一条sql语句
String sql = "select * from t_user where name = '"+loginName+"' and password = '"+loginPwd+"'";
System.out.println(sql);
rs = stmt.executeQuery(sql);
// 处理结果集(结果集中有数据,表示登录成功,反之表示登录失败)
if(rs.next()){
// 登录成功
loginSuccess = true;
// 获取真实的名字
realname = rs.getString("realname");
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 释放资源
DbUtils.close(conn, stmt, rs);
}
System.out.println(loginSuccess ? "登录成功,欢迎" + realname : "登录失败,您的用户名不存在或者密码错误!");
}
}
运行结果:
二、 SQL注入问题
SQL注入问题说的是:用户输入的信息中含有SQL语句关键字,和程序中的SQL语句进行字符串拼接,导致程序中的SQL语句改变了原意。(SQL注入问题是一种系统安全问题)
接下来我们来演示一下SQL注入问题。以用户登录为例。使用表:t_user
业务描述:系统启动后,给出登录页面,用户可以输入用户名和密码,用户名和密码全部正确,则登录成功,反之,则登录失败。
分析一下要执行怎样的SQL语句?是不是这样的?
* 这个程序在实现了基本的登录功能的同时,演示了SQL注入的现象。 * 导致SQL注入现象的根本原因是什么? * 第一:用户提供的信息中含有SQL语句的关键字了。 * 第二:这些用户提供的信息中的关键字参与了SQL语句的编译。 * * SQL注入是一种安全隐患。黑客通常使用SQL注入来攻击你的系统。 * 怎么避免SQL注入现象呢? * java.sql.Statement 是存在SQL注入现象的,因为它的原理是:先拼接SQL语句字符串,然后再进行编译,所以它必然存在SQL注入的问题。 * 为了避免SQL注入的发生,JDBC API中为Statement接口提供了一个子接口:java.sql.PreparedStatement,被称为预编译的数据库操作对象。 * java.sql.PreparedStatement可以解决SQL注入问题。 * 具体怎么解决的?PreparedStatement可以对SQL语句进行预先编译,然后给编译好的SQL语句占位符传值,通过这种方式来解决SQL注入问题。 * 最本质的解决方法是:用户虽然提供了SQL语句关键字,但是这些关键字不再参与SQL语句的编译了。因此解决了SQL注入的问题。
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;
/**
* ClassName: JDBCTest12
* Description: 实现用户登录功能
* 这个程序在实现了基本的登录功能的同时,演示了SQL注入的现象。
* 导致SQL注入现象的根本原因是什么?
* 第一:用户提供的信息中含有SQL语句的关键字了。
* 第二:这些用户提供的信息中的关键字参与了SQL语句的编译。
*
* SQL注入是一种安全隐患。黑客通常使用SQL注入来攻击你的系统。
* 怎么避免SQL注入现象呢?
* java.sql.Statement 是存在SQL注入现象的,因为它的原理是:先拼接SQL语句字符串,然后再进行编译,所以它必然存在SQL注入的问题。
* 为了避免SQL注入的发生,JDBC API中为Statement接口提供了一个子接口:java.sql.PreparedStatement,被称为预编译的数据库操作对象。
* java.sql.PreparedStatement可以解决SQL注入问题。
* 具体怎么解决的?PreparedStatement可以对SQL语句进行预先编译,然后给编译好的SQL语句占位符传值,通过这种方式来解决SQL注入问题。
* 最本质的解决方法是:用户虽然提供了SQL语句关键字,但是这些关键字不再参与SQL语句的编译了。因此解决了SQL注入的问题。
*
*/
public class JDBCTest12 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 初始化登录界面
System.out.println("欢迎使用用户管理系统,请登录!");
System.out.print("用户名:");
String loginName = scanner.nextLine();
System.out.print("密码:");
String loginPwd = scanner.nextLine();
// 连接数据库,验证用户名和密码是否正确。
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
boolean loginSuccess = false;
String realname = null;
try {
// 获取连接
conn = DbUtils.getConnection();
// 获取数据库操作对象
stmt = conn.createStatement();
// 执行SQL语句
//+号是为了拼成一条sql语句
String sql = "select * from t_user where name = '"+loginName+"' and password = '"+loginPwd+"'";
System.out.println(sql);
rs = stmt.executeQuery(sql);
// 处理结果集(结果集中有数据,表示登录成功,反之表示登录失败)
if(rs.next()){
// 登录成功
loginSuccess = true;
// 获取真实的名字
realname = rs.getString("realname");
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 释放资源
DbUtils.close(conn, stmt, rs);
}
System.out.println(loginSuccess ? "登录成功,欢迎" + realname : "登录失败,您的用户名不存在或者密码错误!");
}
}
如果用户名和密码正确的话,执行结果如下:
如果用户名不存在或者密码错误的话,执行结果如下:
接下来,见证奇迹的时刻,当我分别输入以下的用户名和密码时,系统被攻破了:
这种现象就叫做:SQL注入。为什么会发生以上的事儿呢?原因是:用户提供的信息中有SQL语句关键字,并且和底层的SQL字符串进行了拼接,变成了一个全新的SQL语句。
例如:本来程序想表达的是这样的SQL:
select realname from t_user where name = 'zhangsan' and password = '123';
结果被SQL注入之后,SQL语句就变成这样了:
select * from t_user where name = 'zhangsan' and password = 'aasd'or'123'='123'
我们可以执行一下这条SQL,看看结果是什么?
把所有结果全部查到了,这是因为 '1'='1' 是恒成立的,并且使用的是 or 运算符,所以 or 前面的条件等于是没有的。这样就会把所有数据全部查到。而在程序中的判断逻辑是只要结果集中有数据,则表示登录成功。所以以上的输入方式最终的结果就是登录成功。
你设想一下,如果这个系统是一个高级别保密系统,只有登录成功的人才有权限,那么这个系统是不是极其危险了。
三、解决SQL注入问题
导致SQL注入的根本原因是什么?只有找到真正的原因,问题才能得到解决。
最根本的原因是:Statement造成的。先拼串,后编译
Statement执行原理是:先进行字符串的拼接,将拼接好的SQL语句发送给数据库服务器,数据库服务器进行SQL语句的编译,然后执行。因此用户提供的信息中如果含有SQL语句的关键字,那么这些关键字正好参加了SQL语句的编译,所以导致原SQL语句被扭曲。
因此,JDBC为了解决这个问题,引入了一个新的接口:PreparedStatement,我们称为:预编译的数据库操作对象。PreparedStatement是Statement接口的子接口。它俩是继承关系。
PreparedStatement执行原理是:先对SQL语句进行预先的编译,然后再向SQL语句指定的位置传值,
也就是说:用户提供的信息中即使含有SQL语句的关键字,那么这个信息也只会被当做一个值传递给SQL语句,用户提供的信息不再参与SQL语句的编译了,这样就解决了SQL注入问题。
DbUtils类:
public class DbUtils {
/**
* 工具类的构造方法一般都是私有化的,因为工具类中的一般都是静态的,
* 工具类就是为了方便编程,所以工具类中的方法都是直接采用“类名.”
* 的方式访问,因此不需要new对象。
*/
private DbUtils(){}
// 静态变量
private static String driver;
private static String url;
private static String user;
private static String password;
// 静态代码块
static {
// 在这里读取属性配置文件,给静态变量赋值
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
driver = bundle.getString("driver");
url = bundle.getString("url");
user = bundle.getString("user");
password = bundle.getString("password");
// 在类加载的时候,注册驱动,对于整个应用程序来说,注册驱动只需要做一次即可。所以选择静态代码块。
// 静态代码块在类加载时执行,并且只执行一次。
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 获取数据库连接对象
* @return 连接对象
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
// 实际上这里每一次调用 getConnection() 方法时都会获取一个全新的数据库连接对象,实际上这样效率是比较低的,后期会使用连接池进行改造。
Connection conn = DriverManager.getConnection(url, user, password);
return conn;
}
/**
* 释放资源
* @param conn 连接对象
* @param stmt 数据库操作对象
* @param rs 结果集对象
*/
public static void close(Connection conn, Statement stmt, ResultSet rs){
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
测试类:
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;
/**
* ClassName: JDBCTest09
* Description: 使用PreparedStatement解决SQL注入问题。
*
* PreparedStatement原理:
* 第一步:先对SQL语句进行预先编译
* 第二步:给SQL语句中占位符传值
*
* 重点注意事项:
* 在使用预编译的数据库操作对象PreparedStatement时,需要先编写SQL语句,然后再获取PreparedStatement对象。
* 这里编写的SQL语句中,所有“值”的位置都要使用占位符来代替,占位符采用 ?
* 每一个问号 ? 是一个值。是一个占位符。
* 另外,特别要注意:这个占位符问号 ? 两边不能使用单引号或双引号括起来。
*
* 解决SQL注入的本质是:先将带有占位符的SQL语句进行预先编译,然后给占位符传值。
* 即使用户提供的信息中含有SQL语句关键字,但是这些关键字不会参与SQL语句的编译,自然不会扭曲SQL语句的原意。
*/
public class JDBCTest13 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 初始化登录界面
System.out.println("欢迎使用用户管理系统,请登录!");
System.out.print("用户名:");
String loginName = scanner.nextLine();
System.out.print("密码:");
String loginPwd = scanner.nextLine();
// 连接数据库,验证用户名和密码是否正确。
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
boolean loginSuccess = false;
String realname = null;
try {
// 获取连接
conn = DbUtils.getConnection();
// 获取预编译的数据库操作对象
String sql = "select * from t_user where name = ? and password = ?";
ps = conn.prepareStatement(sql);
// 给 ? 占位符 传值
// 再次强调:在JDBC当中,所有的下标都是从1开始。不是从0开始。
// 以下代码的含义是:给第1个占位符 ? 传值
ps.setString(1, loginName);
// 以下代码的含义是:给第2个占位符 ? 传值
ps.setString(2, loginPwd);
// 执行SQL语句
rs = ps.executeQuery();
// 处理结果集(结果集中有数据,表示登录成功,反之表示登录失败)
if(rs.next()){
// 登录成功
loginSuccess = true;
// 获取真实的名字
realname = rs.getString("realname");
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 释放资源
DbUtils.close(conn, ps, rs);
}
System.out.println(loginSuccess ? "登录成功,欢迎" + realname : "登录失败,您的用户名不存在或者密码错误!");
}
}
用户名和密码正确的话,执行结果如下:
用户名和密码错误的话,执行结果如下:
尝试SQL注入,看看还能不能?
通过测试得知,SQL注入问题已经解决了。根本原因是:bbb' or '1'='1 这个字符串中虽然含有SQL语句的关键字,但是只会被当做普通的值传到SQL语句中,并没有参与SQL语句的编译。
关于使用PreparedStatement要注意的是:
占位符使用注意事项
- 占位符
?
不能使用单引号或双引号包裹。如果包裹,占位符则不再是占位符,而是一个普通的问号字符。- 在执行 SQL 语句前,必须给每一个占位符
?
传值。
给占位符传值的方法
通过 pstmt.setXxx(第几个占位符, 传什么值)
来给占位符传值,具体说明如下:
- “第几个占位符”:从 1 开始计数。例如,第 1 个占位符对应的值为 1,第 2 个占位符对应的值为 2,依此类推。
- “传什么值”:具体传值类型取决于调用的方法。
- 如果调用
pstmt.setString
方法,则传的值必须是一个字符串。- 如果调用
pstmt.setInt
方法,则传的值必须是一个整数。- 以此类推......
PreparedStatement和Statement都是用于执行SQL语句的接口,它们的主要区别在于:
Statement是父。PreparedStatement是子
- PreparedStatement预编译SQL语句,Statement直接提交SQL语句;
- PreparedStatement执行速度更快,可以避免SQL注入攻击;(PreparedStatement对于同一条SQL语句来说,编译一次,执行N次。而Statement是每次都要进行编译,因此PreparedStatement效率略微高一些)
- PreparedStatement会做类型检查,是类型安全的;
- Statement适用于处理静态的SQL语句,而PreparedStatement适用于处理在运行时动态生成的SQL语句。
1.PreparedStatement执行速度更快
PreparedStatement
是 Java 中 Statement
接口的子接口,用于执行预编译的 SQL 语句。与普通的 Statement
相比,PreparedStatement
执行速度更快,主要有以下几个原因:
⑴.预编译机制
Statement 的执行方式当使用 Statement 执行 SQL 语句时,每次执行都会将 SQL 语句发送到数据库服务器进行编译。例如:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class StatementExample {
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password");
Statement stmt = conn.createStatement()) {
String sql = "SELECT * FROM users WHERE age = 25";
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
// 处理结果集
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
如果多次执行类似的 SQL 语句,只是参数不同,每次都需要重新编译,这会消耗数据库服务器的资源和时间。
⑵.PreparedStatement 的预编译
PreparedStatement 在创建时会将 SQL 语句发送到数据库服务器进行预编译,生成执行计划。后续执行时,只需将参数传递给预编译的 SQL 语句,无需再次编译。例如:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class PreparedStatementExample {
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password");
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE age = ?")) {
pstmt.setInt(1, 25);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
// 处理结果集
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
预编译的执行计划可以被缓存,当多次执行类似的 SQL 语句时,直接使用缓存的执行计划,避免了重复编译的开销,从而提高了执行速度。
减少网络通信开销
Statement 的网络传输
使用 Statement 时,每次执行 SQL 语句都需要将完整的 SQL 语句(包括参数)从客户端发送到数据库服务器。如果参数较长,会增加网络传输的数据量。
PreparedStatement 的参数传递
PreparedStatement 采用占位符(?)的方式来表示参数,在预编译时只发送 SQL 语句的结构,参数在后续执行时单独传递。这样可以减少网络传输的数据量,特别是当参数是大对象(如大文本、二进制数据)时,这种优势更加明显。
数据库优化
数据库服务器可以对预编译的 SQL 语句进行更有效的优化。由于预编译的 SQL 语句结构固定,数据库可以根据语句的结构和统计信息生成更高效的执行计划,从而提高查询性能。
四、PreparedStatement的使用
1.新增操作
需求:向 emp 表中插入这样一条记录:
empno:8888
ename:张三
job:销售员
mgr:7369
hiredate:2024-01-01
sal:1000.0
comm:500.0
deptno:10
运行前:
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDate;
/**
* ClassName: JDBCTest10
* Description: 使用PreparedStatement完成新增操作
*/
public class JDBCTest14{
public static void main(String[] args) {
/*
需求:向 emp 表中插入这样一条记录:
empno:8888
ename:张三
job:销售员
mgr:7369
hiredate:2024-01-01
sal:1000.0
comm:500.0
deptno:10
*/
Connection conn = null;
PreparedStatement ps = null;
try {
// 1.注册驱动(因为我们使用了DbUtils,因此这一步不需要写)
// 2.获取连接
conn = DbUtils.getConnection();
// 3.获取预编译的数据库操作对象
String sql = "insert into emp(empno,ename,job,mgr,hiredate,sal,comm,deptno) values(?,?,?,?,?,?,?,?)";
// 程序执行到这里,会对“预处理的SQL语句”进行预编译操作
ps = conn.prepareStatement(sql);
// 给 ? 占位符传值
ps.setInt(1, 8888);
ps.setString(2, "张三");
ps.setString(3, "销售员");
ps.setInt(4, 7369);
LocalDate localDate = LocalDate.parse("2024-01-01");
ps.setDate(5, java.sql.Date.valueOf(localDate));
ps.setDouble(6, 1000.0);
ps.setDouble(7, 500.0);
ps.setInt(8, 10);
// 4.执行SQL语句
int count = ps.executeUpdate();
System.out.println("插入了" + count + "条记录");
// 5.处理查询结果集(因为现在是执行insert操作,所以这一步没有)
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 6.释放资源
DbUtils.close(conn, ps, null);
}
}
}
运行结果:
2.修改操作
需求:将员工编号为8888的员工,姓名修改为李四,岗位修改为产品经理,月薪修改为5000.0,其他不变。
运行前:
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* ClassName: JDBCTest11
* Description: 使用PreparedStatement完成修改操作
*/
public class JDBCTest15 {
public static void main(String[] args) {
/*
需求:将员工编号为8888的员工,姓名修改为李四,岗位修改为产品经理,月薪修改为5000.0,其他不变。
*/
Connection conn = null;
PreparedStatement ps = null;
try {
// 1.注册驱动
// 2.获取连接
conn = DbUtils.getConnection();
// 3.获取预编译的数据库操作对象
String sql = "update emp set ename=?, job=?, sal=? where empno=?";
ps = conn.prepareStatement(sql);
ps.setString(1, "李四");
ps.setString(2, "产品经理");
ps.setDouble(3, 5000.0);
ps.setInt(4, 8888);
// 4.执行SQL语句
int count = ps.executeUpdate();
System.out.println("更新了" + count + "条数据");
// 5.处理查询结果集
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// 6.释放资源
DbUtils.close(conn, ps, null);
}
}
}
运行结果:
3. 删除操作
需求:将员工编号为8888的删除。
运行前:
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* ClassName: JDBCTest12
* Description: 使用 PreparedStatement完成删除操作
*/
public class JDBCTest16 {
public static void main(String[] args) {
/*
需求:将员工编号为8888的删除。
使用 try-with-resources 语法。(Java7的新特性)
*/
String sql = "delete from emp where empno = ?";
try(Connection conn = DbUtils.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)){
// 给 ? 传值
ps.setInt(1, 8888);
// 执行SQL语句
int count = ps.executeUpdate();
System.out.println("删除了" + count + "条记录");
}catch(SQLException e){
e.printStackTrace();
}
}
}
运行结果:
4.模糊查询
需求:查询员工名字中第二个字母是 O 的。
运行前
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* ClassName: JDBCTest13
* Description: 使用PreparedStatement完成模糊查询
*/
public class JDBCTest17 {
public static void main(String[] args) {
/*
需求:查询员工名字中第二个字母是 O 的。
*/
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DbUtils.getConnection();
String sql = "select ename from emp where ename like ?";
// 这样是错误的
//String sql = "select ename from emp where ename like '_?%'";
ps = conn.prepareStatement(sql);
ps.setString(1, "_O%");
// 这样是错误的
//ps.setString(1, "O");
rs = ps.executeQuery();
while(rs.next()){
String ename = rs.getString("ename");
System.out.println(ename);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, rs);
}
}
}
运行结果:
通过这个例子主要告诉大家,程序不能这样写:
String sql = "select ename from emp where ename like '_?%'";
pstmt.setString(1, "O");
由于占位符 ? 被单引号包裹,因此这个占位符是无效的。
5.分页查询
对于MySQL来说,通用的分页SQL语句:
假设每页显示3条记录:pageSize = 3
第1页:limit 0, 3
第2页:limit 3, 3
第3页:limit 6, 3
第pageNo页:limit (pageNo - 1)*pageSize, pageSize
需求:查询所有员工姓名,每页显示3条(pageSize),显示第2页(pageNo)。
运行前:
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* ClassName: JDBCTest14
* Description: 使用PreparedStatement完成分页查询
*/
public class JDBCTest18 {
public static void main(String[] args) {
/*
需求:查询所有员工姓名,每页显示3条(pageSize),显示第2页(pageNo)。
*/
// 每页显示的记录条数
int pageSize = 3;
// 显示第几页(页码)
int pageNo = 3;
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DbUtils.getConnection();
String sql = "select ename from emp limit ?, ?";
ps = conn.prepareStatement(sql);
ps.setInt(1, (pageNo - 1) * pageSize);
ps.setInt(2, pageSize);
rs = ps.executeQuery();
while(rs.next()){
String ename = rs.getString("ename");
System.out.println(ename);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, rs);
}
}
}
运行结果:
6.blob数据的插入和读取
准备一张表:t_img,两个字段,一个id主键,一个img。
建表语句如下:
CREATE TABLE `t_img` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`img` MEDIUMBLOB,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
UNSIGNED:表示无符号整数。
准备一张图片:
需求1:向t_img 表中插入一张图片。
运行前
import oop3.utils.DbUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
/**
* ClassName: JDBCTest15
* Description: 使用PreparedStatement向数据库表中插入一张图片。
*/
public class JDBCTest19 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
FileInputStream in = null;
try {
conn = DbUtils.getConnection();
// 获取预编译的数据库操作对象
String sql = "insert into t_img(name, img) values(?, ?)";
ps = conn.prepareStatement(sql);
// 给 ? 传值
ps.setString(1, "狗狗的图片");
in = new FileInputStream("C:\\Users\\86178\\Pictures\\u=1602237319,1596409865&fm=253&fmt=auto&app=138&f=JPEG.webp");
ps.setBlob(2, in);
// 执行SQL
int count = ps.executeUpdate();
System.out.println("插入了" + count + "条记录");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
DbUtils.close(conn, ps, null);
}
}
}
运行结果:
需求2:从t_img 表中读取一张图片。(从数据库中读取一张图片保存到本地。)
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* ClassName: JDBCTest16
* Description: 使用PreparedStatement将表中的图片查询出来。(将数据库中的图片读取到本地硬盘。)
*/
public class JDBCTest20 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DbUtils.getConnection();
String sql = "select img from t_img where name = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "狗狗的图片");
rs = ps.executeQuery();
if(rs.next()){
// 注意:图片就在结果集ResultSet对象当中。
InputStream in = rs.getBinaryStream("img");
OutputStream out = new FileOutputStream("C:\\Users\\86178\\Desktop\\2024\\dog.jpg");
byte[] bytes = new byte[1024];
int readCount = 0;
while((readCount = in.read(bytes)) != -1){
out.write(bytes, 0, readCount);
}
out.flush();
in.close();
out.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, rs);
}
}
}
运行结果:
执行完毕之后,查看一下图片大小是否和原图片相同,打开看看是否可以正常显示。
五、JDBC批处理操作
准备一张商品表:t_product
建表语句如下:
create table t_product(
id bigint primary key,
name varchar(255)
);
1.不使用批处理
不使用批处理,向 t_product 表中插入一万条商品信息,并记录耗时!
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* ClassName: JDBCTest17
* Description: 不使用批处理操作,向t_batch表中插入一万条记录,并且记录耗时。
*/
public class JDBCTest21 {
public static void main(String[] args) {
long begin = System.currentTimeMillis();
Connection conn = null;
PreparedStatement ps = null;
try {
conn = DbUtils.getConnection();
String sql = "insert into t_product(id,name) values(?,?)";
ps = conn.prepareStatement(sql);
int count = 0;
for (int i = 1; i <= 10000; i++) {
ps.setLong(1, i);
ps.setString(2, "batch" + i);
count += ps.executeUpdate();
}
System.out.println("插入了" + count + "条记录");
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, null);
}
long end = System.currentTimeMillis();
System.out.println("总耗时" + (end - begin) + "毫秒"); // 总耗时8096毫秒
}
}
运行结果:
2.使用批处理
使用批处理,向 t_product 表中插入一万条商品信息,并记录耗时!
注意:启用批处理需要在URL后面添加这个的参数:rewriteBatchedStatements=true
import oop3.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
/**
* ClassName: JDBCTest18
* Description:使用批处理操作,向t_batch表中插入一万条记录,并且记录耗时。
*/
public class JDBCTest22 {
public static void main(String[] args) {
long begin = System.currentTimeMillis();
Connection conn = null;
PreparedStatement ps = null;
try {
conn = DbUtils.getConnection();
String sql = "insert into t_product(id,name) values(?,?)";
ps = conn.prepareStatement(sql);
int count = 0;
for (int i = 1; i <= 10000; i++) {
ps.setLong(1, i);
ps.setString(2, "batch" + i);
// 打包
ps.addBatch();
// 如果打包够500个,则执行一次(则磁盘IO一次)
if(i % 500 == 0){
count += ps.executeBatch().length;
}
}
// 循环结束之后,再次执行批处理,防止数据丢失。
count += ps.executeBatch().length;
System.out.println("插入了" + count + "条记录");
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, null);
}
long end = System.currentTimeMillis();
System.out.println("总耗时" + (end - begin) + "毫秒"); // 总耗时924毫秒
}
}
运行结果:
在进行大数据量插入时,批处理为什么可以提高程序的执行效率?
- 减少了网络通信次数:JDBC 批处理会将多个 SQL 语句一次性发送给服务器,减少了客户端和服务器之间的通信次数,从而提高了数据写入的速度,特别是对于远程服务器而言,优化效果更为显著。
- 减少了数据库操作次数:JDBC 批处理会将多个 SQL 语句合并成一条 SQL 语句进行执行,从而减少了数据库操作的次数,减轻了数据库的负担,大大提高了数据写入的速度。
更多推荐
所有评论(0)