04-数据库操作对象Statement对象和PreparedStatement对象的区别,SQL注入的优缺点
Statement对象和查询结果集
Statement对象相关的方法
Connection接口中获取数据库操作对象Statement对象
的方法
方法名 | 功能 |
---|---|
Statement createStatement() | 创建Statement对象 |
Statement对象
执行增删改查的SQL语句(不含占位符"?")
的方法,JDBC中的SQL语句不需要提供分号结尾
方法名 | 功能 |
---|---|
int executeUpdate(insert/delete/update) | 执行dml语句(增删改),返回受影响的行数 |
ResultSet executeQuery(select) | 执行dql语句(查询),返回 ResultSet 结果集对象 |
Connection conn = null;
Statement stmt = null;
//1、注册驱动
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
//2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bjpowernode","root","333");
//3、获取数据库操作对象
stmt = conn.createStatement();
//4、执行SQL语句
// String sql = "delete from dept where deptno = 40";
String sql = "update dept set dname = '销售部', loc = '天津' where deptno = 20";
int count = stmt.executeUpdate(sql);
System.out.println(count == 1 ? "修改成功" : "修改失败");
ResultSet查询结果集的方法
当我们使用数据库操作对象执行查询语句
时会生成ResultSet结果集对象(包含查询到的数据)
,该对象保持一个光标
指向当前的数据行(最初光标位于第一行之前)
ResulrSet结果集
的常用方法: 取出当前行中字段的值时可以以结果集中字段的名称(语义更明确)或字段所在索引(下标从1开始)
作为依据获取
方法名 | 功能 |
---|---|
boolean next() | 最初光标位于第一行之前,执行next方法会让光标向下一行移动,如果没有下一行返回 false |
boolean previous() | 向上移动一行,如果没有上一行则返回false |
String getString(列的索引/字段名) | 不管结果集中字段的数据类型是什么,都以String的形式取出字段值 |
Date getDate(列的索引/字段名) | 以Date的形式取出字段的值 |
int getInt(列的索引/字段名) | 以int的形式取出字段的值 |
Xxx getXxx( 列的索引/字段名 ) | 以指定的数据类型取出结果集中的数据 , 前提是该数据类型可以正常转换 |
Object getObject( 列的索引/字段名 ) | 以对象的形式取出结果集中的数据 |
String sql = "select empno as a,ename,sal from emp";
// 专门执行DQL语句的方法
rs = stmt.executeQuery(sql); 。
// 处理查询结果集
while(rs.next()){
// 以结果集中列的下标获取,JDBC中所有下标从1开始,不是从0开始
String empno = rs.getString(1);
String ename = rs.getString(2);
String sal = rs.getString(3);
System.out.println(empno + "," + ename + "," + sal);
// 以结果集中列的名称获取,列名称不是数据库表中的列名称而是查询结果集的列名称
// 除了以String类型取出之外,还能以特定的类型取出
int empno = rs.getInt("a");
String ename = rs.getString("ename");
double sal = rs.getDouble("sal");
System.out.println(empno + "," + ename + "," + (sal + 200));
}
SQL注入问题
SQL注入的优缺点
SQL注入问题
: 在对SQL语句拼接时不使用占位符
,而是将用户提供的非法信息
直接拼接到到要执行的SQL语句当中,会导致原SQL语句的含义被扭曲了
- 如将
用户名zhangsan
及其密码123456' or '1'='1(两个用or连接的条件去掉两边的引号)
拼接到SQL语句当中 字符串拼接变量或表达式的方法
: 先加双引号中间加两个+
号,两个加号中间加表达式
String loginName = userLoginInfo.get("loginName");
String loginPwd = userLoginInfo.get("loginPwd");
// select * from t_user where loginName = 'zhangsan' and loginPwd = '123456' or '1'='1';
String sql = "select * from t_user where loginName = '"+loginName+"' and loginPwd = '"+loginPwd+"'";
// 完成了sql语句的拼接后发给DBMS,然后DBMS对拼接好的sql语句进行编译
rs = stmt.executeQuery(sql);
SQL注入的用途
:凡是业务方面要求进行SQL语句拼接的必须使用Statement对象,如用户在控制台输入desc/asc决定降序/升序
// 用户在控制台输入desc就是降序,输入asc就是升序
Scanner s = new Scanner(System.in);
System.out.println("输入desc或asc,desc表示降序,asc表示升序");
System.out.print("请输入:");
String keyWords = s.nextLine();
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
// 注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bjpowernode", "root","333");
String sql = "select ename from emp order by ename ? " ;
// 给占位符传值后的结果select ename from emp order by ename 'desc或asc'(sql语法错误)
ps = conn.prepareStatement(sql);
ps.setString(1, keyWords);
// 使用Statement对象进行sql语句拼接时不会出现问题
stmt = conn.createStatement();
String sql = "select ename from emp order by ename " + keyWords;
rs = stmt.executeQuery(sql);
// 遍历结果集
while(rs.next()){
System.out.println(rs.getString("ename"));
}
PreparedStatement对象和查询结果集
PreparedStatement相关方法
Connection接口
中获取数据库操作对象PreparedStatement对象
的方法,创建对象的同时对SQL语句进行预编译
方法名 | 功能 |
---|---|
PreparedStatement prepareStatement(sql) | 创建预处理对象 |
PreparedStatement接口
继承了java.sql.Statement
,是预编译的数据库操作对象(SQL语句含占位符),可以解决SQL注入问题
- 预先对
包含占位符的SQL语句
进行编译,然后再给占位符传值,即使用户提供的信息中含有sql语句关键字也无法参与编译过程,最终被当作普通的字符处理
将预编译的SQL语句的参数用占位符?(不能使用单引号括起来)
表示,一个?
表示一个占位符,最终调用ps对象的setXxx()方法
给占位符传值(下标从1开始
)
一个PreparedStatement对象每次只能预编译一条SQL语句
并且在编译阶段会做类型的安全检查
数据库第一次执行SQL语句时会先进行编译,如果第二次执行时SQL语句没有任何变化则直接执行不再编译
Statement对象
: 编译一次执行一次且每次执行的是一个完整的SQL语句PreparedStatement对象(效率较高)
: 预先对含占位符SQL语句的编译,然后调用ps对象的setXxx()方法给占位符传值,同一个SQL模板编译一次可执行N次
PreparedStatement接口
中的方法: 方法的参数中不能再写SQL语句,否则就会重新编译SQL语句
方法名 | 功能 |
---|---|
int executeUpdate() | 执行dml语句(增删改),返回受影响的行数 |
ResultSet executeQuery() | 执行dql语句(查询),返回 ResultSet 结果集对象 |
execute() | 执行任意的sql,返回布尔值 |
void setXxx(占位符索引 , 占位符的值) | 给占位符设置对应类型的值,占位符下标从1开始 |
void setString(占位符索引 , 占位符的值) | 给占位符设置的值在sql语句中被当作字符串处理(自动加引号) |
void setInt(占位符索引 , 占位符的值) | 给占位符设置的值在sql语句中被当作int类型的数据处理(不会加引号) |
执行DQL
语句
// 一个?表示一个占位符,一个?将来可以接收一个值
String sql = "select * from t_user where loginName = ? and loginPwd = ?";
// 程序执行到此处会将占位符的sql语句发送给DBMS,然后DBMS对该sql语句进行预编译
ps = conn.prepareStatement(sql);
// 给占位符?传值(第1个问号下标是1,第2个问号下标是2,JDBC中所有下标从1开始)
ps.setString(1, loginName);
ps.setString(2, loginPwd);
// 执行sql,ps已经预编译过了sql语句,如果再传就会重新编译sql语句
rs = ps.executeQuery();
// 处理结果集
if(rs.next()){
// 登录成功
loginSuccess = true;
}
执行DML
语句
// 执行插入语句
String sql = "insert into dept(deptno,dname,loc) values(?,?,?)";
ps = conn.prepareStatement(sql);
ps.setInt(1, 60);
ps.setString(2, "销售部");
ps.setString(3, "上海");
// 执行更新语句
String sql = "update dept set dname = ?, loc = ? where deptno = ?";
ps2 = conn.prepareStatement(sql);
ps2.setString(1, "研发一部");
ps2.setString(2, "北京");
ps2.setInt(3, 60);
// 执行删除语句
String sql = "delete from dept where deptno = ?";
ps3 = conn.prepareStatement(sql);
ps3.setInt(1, 60);
System.out.println(count);
模糊查询
查找第二个字母包含A的员工
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
// 使用工具类获取连接
conn = DBUtil.getConnection();
// 以下是错误的写法,?一定不能用单引号括起来
String sql = "select ename from emp where ename like '_?%'";
ps = conn.prepareStatement(sql);
ps.setString(1, "A");
// 正确写法
String sql = "select ename from emp where ename like ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "_A%");
rs = ps.executeQuery();
while(rs.next()){
System.out.println(rs.getString("ename"));
}
模拟用户登录功能(防止注入)
需求: 从数据库中查询用户信息,实现用户登陆功能
public class JDBCTest {
public static void main(String[] args) {
// 初始化一个界面
Map<String,String> userLoginInfo = initUI();
// 验证用户名和密码
boolean loginSuccess = login(userLoginInfo);
// 最后输出结果
System.out.println(loginSuccess ? "登录成功" : "登录失败");
}
}
第一步: 初始化用户界面
,可以让用户输入的用户名和密码等登录信息
private static Map<String, String> initUI() {
Scanner s = new Scanner(System.in);
System.out.print("用户名:");
String loginName = s.nextLine();
System.out.print("密码:");
String loginPwd = s.nextLine();
Map<String,String> userLoginInfo = new HashMap<>();
userLoginInfo.put("loginName", loginName);
userLoginInfo.put("loginPwd", loginPwd);
return userLoginInfo;
}
第二步: 实现用户登陆
的业务逻辑
private static boolean login(Map<String, String> userLoginInfo) {
// false表示失败,true表示成功
boolean loginSuccess = false;
// 单独定义变量
String loginName = userLoginInfo.get("loginName");
String loginPwd = userLoginInfo.get("loginPwd");
// 获取预编译的数据库操作对象PreparedStatement
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2、获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bjpowernode", "root", "333");
// 3、获取预编译的数据库操作对象同时对SQL语句进行预编译
String sql = "select * from t_user where loginName = ? and loginPwd = ?";
ps = conn.prepareStatement(sql);
// 给占位符?传值,下标从一开始
ps.setString(1, loginName);
ps.setString(2, loginPwd);
// 4、执行sql
rs = ps.executeQuery();
// 5、处理结果集
if(rs.next()){
// 登录成功
loginSuccess = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6、释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return loginSuccess;
}