Java JDBC

JDBC 是一套 Java 用于连接关系型数据库的接口。

一、数据库基础

具体请看:

分类 - SQL分类 - MySQL

二、什么是 JDBC ?

1. JDBC

JDBC,Java Database Connectivity,即 Java 数据库连接,是一套操作关系型数据库的接口。

2. 本质是规则

JDBC 的本质是一套操作所有关系型数据库的规则(接口)。各个数据库厂商需要实现这一套接口,以便 Java 程序能够以相同的方式访问所有关系型数据库。

3. 功能概述

JDBC 可以完成以下三个基本功能:

  • 建立与数据库的连接
  • 执行 SQL 语句
  • 获取 SQL 语句的执行结果

三、步骤

1. 导入驱动 jar 包

  • 下载对应数据库的 jar 包

    MySQL :: Download Connector/J

  • 复制 jar 包至项目文件夹下 libs 目录

  • 选中 jar 包,右键 Add As Library

2. 注册驱动

最新的 JDBC 驱动已经可以通过 SPI 自动注册驱动类

驱动包的 META-INF\services 路径下会包含 Java.sql.Driver 文件,此文件指定了 JDBC 驱动类

1
Class.forName(驱动类名)

3. 获取 Connection 对象

使用 DriverManager 的 getConnection() 类方法获取数据库连接。

1
Connection connection = DriverManager.getConnection(String url, String username, String password)
  • url:数据库链接

    1
    jdbc:mysql://IP地址:端口号/数据库名称
  • username:用户名

  • password:密码

1
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/数据库名称", "root", "root")

4. 获取 Statement 对象

通过 Connection 对象的实例方法创建 Statement 对象,方法有三种:

  • createStatement:创建基本的 Statement 对象
  • prepareStatement(String sql):根据传入的 sql 创建预编译的 Statement 对象
  • prepareCall(String sql):根据传入的 sql 创建 CallableStatement 对象
1
Statement statement = Connection对象.createStatement()

5. 执行 SQL

通过 Statement 对象的实例方法执行 SQL 语句,方法有以下三种:

  • execute():可以执行任何 SQL 语句
  • executeUpdate():可以执行增、删、改、修改表结构的语句
    • 执行增、删、改语句后,返回受此 SQL 语句影响的行数
    • 执行修改表结构语句后,返回 0
  • executeQuery():可以执行查询语句,返回代表查询结果的 ResultSet 对象

6. 操作 ResultSet 对象

如果在上一步执行了查询语句,便可以获得代表查询结果的 ResultSet 对象,并通过操作它取出结果。

ResultSet 对象提供以下两类方法:

7. 回收资源

在程序结束后,应该回收资源,包括 ResultSet、Statement 和 Connection 等资源。

四、PreparedStatement 对象

1. 优势

  • 对 SQL 语句进行了预编译,性能更好
  • 可以防止 SQL 注入攻击
  • 无需拼接 SQL 语句,编程更简单

2. 使用方法

  • 定义 sql 语句,其中未知参数使用 占位

    1
    String sql = "select * from user where username = ? and password = ?;
  • 通过 Connection 对象的 prepareStatement(String sql) 方法,传入 sql 语句,创建预编译的 Statement 对象

  • 通过 PreparedStatement 对象的 setXxx(int index, Xxx value) 方法为参数赋值

    其中:

    • index 代表第几个参数
    • value 为要向其中填入的值

    需要注意的是:

    • 如果明确知道参数的类型,可以使用对应的 setXxx() 方法

    • 如果不清楚参数的类型,可以使用 setObject() 方法传入参数,PreparedStatement 会自动进行类型转换

  • 通过 PreparedStatement 对象的实例方法执行 SQL

    需要传入所有参数之后才可以执行,并且执行时无需传入参数,直接调用方法即可。

    方法有以下三种:

    • execute():可以执行任何 SQL 语句
    • executeUpdate():可以执行增、删、改、修改表结构的语句
      • 执行增、删、改语句后,返回受此 SQL 语句影响的行数
      • 执行修改表结构语句后,返回 0
    • executeQuery():可以执行查询语句,返回代表查询结果的 ResultSet 对象

3. SQL 注入攻击

(1) 案例

需求:

用户输入账户名和密码,程序通过 JDBC 查询用户表中是否有对应数据。

如果有,返回登陆成功;如果没有,返回登陆失败。

数据表:

代码:

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
public class Test {
public static void main(String[] args) throws SQLException {
// 接受用户输入
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
// 调用方法并根据返回值输出语句
System.out.println(login(username, password) ? "登录成功" : "登录失败");
}

// 登录方法
public static boolean login(String username, String password) throws SQLException {
if (username == null || password == null) {
return false;
}
// 获取 Connection 对象
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "root", "root");
// 获取 Statement 对象
Statement statement = connection.createStatement();
// 拼接 sql 语句
String sql = "select * from user where username = '" + username + "' and password = '" + password + "'";
System.out.println(sql);
// 执行查询,获得 ResultSet 对象
ResultSet resultSet = statement.executeQuery(sql);
// 返回 ResultSet 对象是否有值,即表中是否有对应数据
return resultSet.next();
}
}

运行结果:

(2) 漏洞

由于 sql 语句是由原语句和用户输入信息通过字符串拼接而成,因此恶意用户可以通过字符串拼接使得 sql 语句执行得到错误结果,导致程序误认为恶意用户具有登录权限。

3. SQL 注入攻击的解决

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
public class Test {
public static void main(String[] args) throws SQLException {
// 接受用户输入
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
// 调用方法并根据返回值输出语句
System.out.println(betterLogin(username, password) ? "登录成功" : "登录失败");
}

public static boolean betterLogin(String username, String password) throws SQLException {
if (username == null || password == null) {
return false;
}
// 获取 Connection 对象
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "root", "root");
// 定义 sql 语句
String sql = "select * from user where username = ? and password = ?";
// 获取 prepareStatement 对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 为参数赋值
preparedStatement.setString(1, username);
preparedStatement.setString(2, password);
// 执行查询,获得 ResultSet 对象
ResultSet resultSet = preparedStatement.executeQuery();
// 返回 ResultSet 对象是否有值,即表中是否有对应数据
return resultSet.next();
}
}

4. PreparedStatement 如何防止 SQL 注入?

由上述案例可以发现,使用 PreparedStatement 可以有效地防止 SQL 注入。那么 PreparedStatement 如何防止 SQL 注入?

查看源码可以发现,PreparedStatement 的做法是:

  • 首先对传入参数的所有单引号做转义

  • 在传入参数的两边加上单引号

  • 将传入参数与 SQL 语句进行拼接

通过对传入参数的转义实现了防止 SQL 注入。

五、CallableStatement 对象

1. 什么是存储过程?

MySQL 存储过程

存储过程是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。

存储过程是为了完成特定功能的 SQL 语句集,经编译创建并保存在数据库中,用户可以通过指定存储过程的名字并给定参数来调用执行。

存储过程的本质是 SQL 语言的代码封装和复用。

2. 创建 CallableStatement 对象

通过 Connection 的 prepareCall() 方法创建 CallableStatement 对象,创建该对象时需要传入调用存储过程的 SQL 语句。

调用存储过程的 SQL 语句通过为以下格式:

1
{call 过程名(?, ?, ?...)}

3. 存储过程的参数

存储过程的参数既有传入参数,也有传出参数。

  • 传入参数:通过 CallableStatement 对象的 setXxx() 方法设置
  • 传出参数:可以通过该参数获取存储过程中的值,需要调用 CallableStatement 对象的 registerOutParametar() 方法注册此参数

4. 执行

通过 CallableStatement 对象的 execute() 方法执行存储过程。

5. 获取结果

通过 CallableStatement 对象的 getXxx(int index) 方法获取指定传出参数的值。

六、执行 SQL

1. executeUpdate()

可以使用 executeUpdate() 执行增、删、改、修改表结构的语句

返回值:

  • 执行增、删、改语句后,返回受此 sql 语句影响的行数
  • 执行修改表结构语句后,返回 0

2. execute()

可以使用 execute() 执行任何 SQL 语句,但使用该方法较为麻烦,适用于无法预知 SQL 语句类型的场景。

该方法将会返回一个 Boolean 值,用于表示语句执行后是否获得了 ResultSet 对象,可以通过以下方法获取执行结果:

  • getResultSet() :获取 ResultSet 对象
  • getUpdateCount() :获取语句执行后受影响的函数

常用做法:

1
2
3
4
5
6
7
8
9
10
···
boolean hasResultSet = Statement对象.execute(sql);
if (hasResultSet) {
// 说明执行了查询语句,获取其结果集
Result result = Statement对象.getResultSet();
···
}else {
// 说明执行了增、删、改、修改表结构的语句,获取其影响行数
int count = Statement对象.getUpdateCount()
}

七、控制事务

1. 事务

数据库 事务

2. JDBC 的事务支持

JDBC 的事务支持由 Connection 提供。

3. 开启事务

Connection 默认打开自动提交,即关闭事务。此时,每条 SQL 语句一旦被 Statement 对象执行,便会立即提交到数据库并执行。

可以调用 Connection 对象的 setAutoCommit() 方法关闭自动提交、开启事务:

1
connection.setAutoCommit(false);

可以通过调用 Connection 对象的 getAutoCommit() 方法来获得 Connection 对象的自动提交版本

4. 语句执行

事务开启之后,可以正常创建 Statement 对象,并执行增、删、改、查语句。

这些语句虽然被 Statement 对象执行,但并不会立即被数据库执行。

5. 提交

可以通过 Connection 对象的 commit() 方法提交事务。

6. 回滚

可以通过 Connection 对象的 rollball() 方法回滚事务。

当运行中遇到未处理的 SQLException 异常时,系统将会非正常退出,事务也会自动回滚。

当异常被捕获时,需要在异常处理块中显式回滚事务。

八、连接池

1. 为什么需要连接池?

数据库连接的建立和关闭是极耗费系统资源的操作,在多层结构的应用环境下,这种资源的耗费对系统性能影响尤为明显。

如果通过平常的方式控制数据库,每一次执行前都首先获取连接,每一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完毕后立即关闭连接,会导致系统性能低下。

此时便引入了数据库连接池技术。

2. 连接池解决方案

  • 当应用程序启动时,系统主动建立足够的数据库连接,并将这些连接组成一个连接池

  • 每次请求数据库连接时,无需重新打开连接,而是从连接池中取出已有的连接进行使用

  • 使用完成后不再关闭数据库连接,而是将连接归还给连接池

    如果 Connection 对象是从连接池中获取的,那么调用 close() 方法将不会关闭连接,而是会将连接归还给连接池

3. DataSource 接口

JDBC 的数据库连接池使用 Javax.sql.DataSource 接口表示,该接口由数据库厂商或开源组织实现。

参考