对于JDBC核心API的理解

JDBC核心API理解

数据库代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE DATABASE xy;

use xy;

create table t_emp
(
emp_id int auto_increment comment '员工编号' primary key,
emp_name varchar(100) not null comment '员工姓名',
emp_salary double(10, 5) not null comment '员工薪资',
emp_age int not null comment '员工年龄'
);

insert into t_emp (emp_name,emp_salary,emp_age)
values ('andy', 777.77, 32),
('大风哥', 666.66, 41),
('康师傅',111, 23),
('Gavin',123, 26),
('小鱼儿', 123, 28);

Java代码实现

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
package com.xy.base;

import java.sql.*;
@SuppressWarnings({"all"})
public class JDBCQick {
public static void main(String[] args) throws Exception {

//1.注册驱动:从JDBC4.0开始,驱动注册是自动完成的
//Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接对象
String url = "jdbc:mysql://localhost:3306/xy";
String username = "root";
String password = "230320";
Connection connection = DriverManager.getConnection(url, username, password);

//3.获取执行SQL语句的对象
Statement statement = connection.createStatement();

//4.编写SQL语句,并执行,接收返回的结果集
String sql = "select emp_id,emp_name,emp_salary,emp_age from t_emp";
ResultSet resultSet = statement.executeQuery(sql);

//5.处理结果:遍历resultSet结果集
while (resultSet.next()) {
int emp_id = resultSet.getInt("emp_id");
String emp_name = resultSet.getString("emp_name");
int emp_salary = resultSet.getInt("emp_salary");
int emp_age = resultSet.getInt("emp_age");
System.out.println(emp_id + "\t" + emp_name + "\t" + emp_salary + "\t" + emp_age);
}
//6.释放资源(先开后关原则)
resultSet.close();
statement.close();
connection.close();
}
}

步骤总结

  1. 注册驱动【依赖的驱动类,进行安装】
  2. 获取连接【Connection建立连接】
  3. 创建发送SQL语句对象【Connection创建发送SQL语句的Statement】
  4. 发送SQL语句,并获取返回结果【Statement 发送sql语句到数据库并且取得返回结果】
  5. 结果集解析【结果集解析,将查询结果解析出来】
  6. 资源关闭【释放ResultSet、Statement 、Connection】

核心API理解

注册驱动

1
Class.forName("com.mysql.cj.jdbc.Driver");
  • 在 Java 中,当使用 JDBC(Java Database Connectivity)连接数据库时,需要加载数据库特定的驱动程序,以便与数据库进行通信。加载驱动程序的目的是为了注册驱动程序,使得 JDBC API 能够识别并与特定的数据库进行交互。
  • 从JDK6开始,不再需要显式地调用 Class.forName() 来加载 JDBC 驱动程序,只要在类路径中集成了对应的jar文件,会自动在初始化时注册驱动程序。

Connection

  • Connection接口是JDBC API的重要接口,用于建立与数据库的通信通道。换而言之,Connection对象不为空,则代表一次数据库连接。

  • 在建立连接时,需要指定数据库URL、用户名、密码参数。

    • URL:jdbc:mysql://localhost:3306/xy
      • jdbc:mysql://IP地址:端口号/数据库名称?参数键值对1&参数键值对2

    这里补充一点当你在自己的电脑的本地端口时,URL可以为:

    URL:jdbc:mysql:///xy

  • Connection 接口还负责管理事务,Connection 接口提供了 commitrollback 方法,用于提交事务和回滚事务。

  • 可以创建 Statement 对象,用于执行 SQL 语句并与数据库进行交互。

  • 在使用JDBC技术时,必须要先获取Connection对象,在使用完毕后,要释放资源,避免资源占用浪费及泄漏。

获取 Connection 对象就是连接数据库

  • 当你调用 DriverManager.getConnection(url, username, password) 时,JDBC 会与数据库建立一次连接。
  • 这个连接是通过 Connection 对象表示的。如果 Connection 对象不为空(即 connection != null),说明连接成功。
  • 每次获取 Connection 对象,都会占用数据库的一个连接资源。

为什么要释放 Connection 资源?

数据库连接是一种有限的资源,如果不及时释放,可能会导致以下问题:

  1. 资源浪费
  • 每个 Connection 对象都会占用数据库的连接资源。
  • 如果连接不释放,数据库的连接池可能会被耗尽,导致新的连接请求失败。
  1. 资源泄漏
  • 如果程序中没有正确关闭 Connection,这些连接会一直保持打开状态,最终可能导致数据库服务器崩溃。
  1. 性能问题
  • 未释放的连接会占用内存和网络资源,影响应用程序的性能。

Statement

  • Statement 接口用于执行 SQL 语句并与数据库进行交互。它是 JDBC API 中的一个重要接口。通过 Statement 对象,可以向数据库发送 SQL 语句并获取执行结果。
  • 结果可以是一个或多个结果。
    • 增删改:受影响行数单个结果。
    • 查询:单行单列、多行多列、单行多列等结果。
  • 但是Statement 接口在执行SQL语句时,会产生SQL注入攻击问题:
    • 当使用 Statement 执行动态构建的 SQL 查询时,往往需要将查询条件与 SQL 语句拼接在一起,直接将参数和SQL语句一并生成,让SQL的查询条件始终为true得到结果。

关于SQL注入攻击问题:

DBC0

当你输入abc' OR '1' = '1时,由于数据库中没有abc' OR '1' = '1没有这个字段,因此输出应该为空才对,那为什么输出却是整个数据库呢?

很简单,这是字符串的拼接问题,如图所示:

DBC0

总结:

代码分析

代码功能

  • 这段代码的功能是根据用户输入的员工姓名,查询 t_emp 表中的员工信息。
  • 用户输入的姓名会被拼接到 SQL 语句中,然后通过 Statement 执行查询。

关键代码

1
2
3
String name = scanner.nextLine();
String s = "select emp_id,emp_name,emp_salary,emp_age from t_emp where emp_name='" + name + "'";
ResultSet rs = stmt.executeQuery(s);
  • 用户输入的 name 被直接拼接到 SQL 语句中。

  • 如果用户输入 abc' OR '1' = '1,最终的 SQL 语句会变成:

    1
    SELECT emp_id, emp_name, emp_salary, emp_age FROM t_emp WHERE emp_name='abc' OR '1' = '1';
  • 由于 '1' = '1' 永远为真,这条 SQL 语句会返回 t_emp 表中的所有数据。


SQL 注入的原理

什么是 SQL 注入?

SQL 注入是一种攻击技术,攻击者通过在用户输入中插入恶意 SQL 代码,破坏原始 SQL 语句的结构,从而执行非预期的操作。

攻击过程

  • 攻击者输入 abc' OR '1' = '1

  • 这段输入被拼接到 SQL 语句中,导致 SQL 语句的逻辑被改变。

  • 最终的 SQL 语句:

    1
    SELECT emp_id, emp_name, emp_salary, emp_age FROM t_emp WHERE emp_name='abc' OR '1' = '1';
  • 由于 OR '1' = '1' 永远为真,查询条件被绕过,返回所有数据。

危害

  • 攻击者可以获取数据库中的所有数据,包括敏感信息。
  • 攻击者可以执行任意 SQL 语句,如删除表、修改数据等。

PreparedStatement

  • PreparedStatementStatement 接口的子接口,用于执行预编译的 SQL 查询,作用如下:
    • 预编译SQL语句:在创建PreparedStatement时,就会预编译SQL语句,也就是SQL语句已经固定。
    • 防止SQL注入:PreparedStatement 支持参数化查询,将数据作为参数传递到SQL语句中,采用?占位符的方式,将传入的参数用一对单引号包裹起来’’,无论传递什么都作为值。有效防止传入关键字或值导致SQL注入问题。
    • 性能提升:PreparedStatement是预编译SQL语句,同一SQL语句多次执行的情况下,可以复用,不必每次重新编译和解析。
  • 后续的学习我们都是基于PreparedStatement进行实现,更安全、效率更高!
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.xy.base;

import java.sql.*;
import java.util.Scanner;

public class JDBCPrepared {
public static void main(String[] args) throws Exception {
//1.注册驱动(可以省略)

//2.获取连接对象
Connection connection = DriverManager.getConnection("jdbc:mysql:///xy", "root", "230320");

//3.获取并执行SQL语句
PreparedStatement preparedStatement = connection.prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp where emp_name = ?");

System.out.println("请输入员工姓名:"); //abc' OR '1' = '1
//?代表一对单引号''
//abc' OR '1' = '1 ————> 'abc\' or \'1\' = \'1'
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();

//4.为?占位符赋值,并执行SQL语句,接收返回值
preparedStatement.setString(1, name);
ResultSet rs = preparedStatement.executeQuery();

//5.处理结果:遍历resultSet结果集
while (rs.next()) {
int emp_id = rs.getInt("emp_id");
String emp_name = rs.getString("emp_name");
int emp_salary = rs.getInt("emp_salary");
int emp_age = rs.getInt("emp_age");
System.out.println(emp_id + " " + emp_name + " " + emp_salary + " " + emp_age);
}

//6.释放资源(先开后关原则)
rs.close();
preparedStatement.close();
connection.close();
}
}
  1. 什么是 PreparedStatement?

PreparedStatement 是 Java JDBC 中用于执行预编译 SQL 语句的接口。与 Statement 不同,PreparedStatement 允许你在 SQL 语句中使用占位符 ?,然后将具体的参数值动态地绑定到这些占位符上。


  1. ? 的作用

? 是 SQL 语句中的占位符,表示一个参数的位置。在执行 SQL 语句之前,你需要通过 PreparedStatement 的方法(如 setStringsetInt 等)将具体的值绑定到这些占位符上。

例如:

1
2
3
4
String sql = "SELECT * FROM t_emp WHERE emp_name = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, "Andy"); // 将 "Andy" 绑定到第一个占位符
//值得一提的是,这个?排第几个位置就写数字几个,而不是像数组下表一样从0开始

  1. ? 的机制详解

(1) SQL 语句的预编译

  • 当你调用 connection.prepareStatement(sql) 时,JDBC 会将 SQL 语句发送到数据库进行预编译
  • 预编译后的 SQL 语句会被数据库缓存,后续执行时可以直接使用,无需重新解析和编译,从而提高性能。

(2) 占位符的绑定

  • 在预编译阶段,? 只是一个占位符,不会被解析为具体的值。
  • 在执行 SQL 语句之前,你需要通过 PreparedStatement 的方法(如 setStringsetInt 等)将具体的值绑定到占位符上。
  • 绑定值时,JDBC 会确保值被正确处理,避免 SQL 注入。

(3) 参数的类型安全

  • PreparedStatement 提供了多种 setXxx 方法(如 setStringsetIntsetDouble 等),用于绑定不同类型的参数。
  • 这些方法会确保参数值被正确地转换为 SQL 语句中的数据类型。

(4) 防止 SQL 注入

  • 当使用 PreparedStatement 时,JDBC 会自动对绑定的值进行转义处理,确保它们不会被解释为 SQL 代码。
  • 例如,如果用户输入的字符串中包含单引号 ',JDBC 会将其转义为 \',从而避免破坏 SQL 语句的结构。

  1. PreparedStatement 的工作流程

以下是一个完整的 PreparedStatement 使用流程:

(1) 定义 SQL 语句

1
String sql = "SELECT * FROM t_emp WHERE emp_name = ?";

(2) 创建 PreparedStatement 对象

1
PreparedStatement preparedStatement = connection.prepareStatement(sql);

(3) 绑定参数值

1
preparedStatement.setString(1, "Andy");
  • setString(1, "Andy"):将 "Andy" 绑定到第一个占位符(?)。

  • 如果有多个占位符,可以依次绑定:

    1
    2
    preparedStatement.setString(1, "Andy");
    preparedStatement.setInt(2, 30);

(4) 执行 SQL 语句

1
ResultSet resultSet = preparedStatement.executeQuery();

(5) 处理结果

1
2
3
4
5
6
7
while (resultSet.next()) {
int emp_id = resultSet.getInt("emp_id");
String emp_name = resultSet.getString("emp_name");
double emp_salary = resultSet.getDouble("emp_salary");
int emp_age = resultSet.getInt("emp_age");
System.out.println(emp_id + "\t" + emp_name + "\t" + emp_salary + "\t" + emp_age);
}

(6) 释放资源

1
2
resultSet.close();
preparedStatement.close();

  1. 为什么 PreparedStatement 能防止 SQL 注入?

(1) SQL 注入的原理

SQL 注入是通过在用户输入中插入恶意 SQL 代码,破坏原始 SQL 语句的结构,从而执行非预期的操作。例如:

1
SELECT * FROM t_emp WHERE emp_name = 'abc' OR '1' = '1';
  • 如果用户输入 abc' OR '1' = '1,原始 SQL 语句会被修改为:

    1
    SELECT * FROM t_emp WHERE emp_name = 'abc' OR '1' = '1';
  • 由于 '1' = '1' 永远为真,这条 SQL 语句会返回所有员工的数据。

(2) PreparedStatement 的防护机制

  • 当使用 PreparedStatement 时,JDBC 会将用户输入的值作为参数处理,而不是直接拼接到 SQL 语句中。

  • 例如:

    1
    2
    3
    String sql = "SELECT * FROM t_emp WHERE emp_name = ?";
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    preparedStatement.setString(1, "abc' OR '1' = '1");
    • JDBC 会将 "abc' OR '1' = '1" 转义为 "abc\' OR \'1\' = \'1",然后将其作为字符串值绑定到 SQL 语句中。

    • 最终执行的 SQL 语句是:

      1
      SELECT * FROM t_emp WHERE emp_name = 'abc\' OR \'1\' = \'1';
    • 这样,恶意代码就不会被解释为 SQL 语法,从而避免了 SQL 注入。


  1. PreparedStatement 的优势

  2. 防止 SQL 注入

    • 通过参数绑定机制,确保用户输入不会被解释为 SQL 代码。
  3. 性能优化

    • SQL 语句在数据库中被预编译,后续执行时可以直接使用缓存,减少解析和编译的开销。
  4. 类型安全

    • 通过 setXxx 方法绑定参数,确保参数值与 SQL 语句中的数据类型一致。
  5. 代码可读性

  • 使用占位符 ? 可以使 SQL 语句更清晰,避免字符串拼接的混乱。

ResultSet

  • ResultSet是 JDBC API 中的一个接口,用于表示从数据库中执行查询语句所返回的结果集。它提供了一种用于遍历和访问查询结果的方式。
  • 遍历结果:ResultSet可以使用 next() 方法将游标移动到结果集的下一行,逐行遍历数据库查询的结果,返回值为boolean类型,true代表有下一行结果,false则代表没有。
  • 获取单列结果:可以通过getXxx的方法获取单列的数据,该方法为重载方法,支持索引和列名进行获取。