JDBC
JDBC 概述
JDBC 是 Java 提供的一套数据库访问规范,主要位于 java.sql 和 javax.sql 包中。数据库厂商通过驱动程序实现 JDBC 接口,Java 程序面向统一接口编写代码,从而连接不同类型的关系型数据库。
JDBC 常用于执行以下操作:
- 建立和关闭数据库连接。
- 执行新增、修改、删除和查询语句。
- 处理查询结果集。
- 控制事务。
- 获取数据库及结果集的元数据。
准备 MySQL 驱动
使用 JDBC 连接 MySQL 前,需要将 MySQL Connector/J 驱动加入项目依赖。
在传统 Java 工程中,可以把驱动 JAR 文件放入项目的 lib 目录,并添加到类库。在 Maven 项目中,应通过项目依赖管理驱动版本。
MySQL 8 常用驱动类名如下:
com.mysql.cj.jdbc.Driver
现代 JDBC 驱动通常支持自动注册,但显式加载驱动仍有助于理解 JDBC 的基本流程。
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new RuntimeException("未找到 MySQL JDBC 驱动", e);
}
建立数据库连接
使用 DriverManager.getConnection() 创建 Connection 对象。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionTest {
public static void main(String[] args) {
String url =
"jdbc:mysql://localhost:3306/java2601" +
"?serverTimezone=UTC&characterEncoding=utf8";
String username = "root";
String password = "root";
try (Connection connection = DriverManager.getConnection(
url,
username,
password
)) {
System.out.println("数据库连接成功:" + !connection.isClosed());
} catch (SQLException e) {
e.printStackTrace();
}
}
}JDBC URL 中常见的配置包括用户名、密码、时区、字符编码和 SSL 设置。用户名和密码通常作为 getConnection() 的独立参数传入,不建议直接写入 URL。
数据库账号和密码不应硬编码在公开仓库中。真实项目通常使用配置文件、环境变量或密钥管理服务。
JDBC 基本编程步骤
以向 employee 表插入一条记录为例,基本步骤如下:
加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
获取连接
Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/java2601?serverTimezone=UTC&characterEncoding=utf8",
"root",
"root"
);
创建 SQL 执行对象
推荐使用 PreparedStatement。
String sql = "insert into employee(first_name, salary, department_id) " +
"values(?, ?, ?)";
PreparedStatement statement = connection.prepareStatement(sql);
设置参数并执行
statement.setString(1, "Tom");
statement.setDouble(2, 8000);
statement.setInt(3, 5001);
int affectedRows = statement.executeUpdate();
System.out.println("受影响行数:" + affectedRows);
关闭资源
Connection、Statement 和 ResultSet 都属于需要关闭的资源。推荐使用 try-with-resources 自动关闭。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class InsertEmployee {
public static void main(String[] args) {
String url =
"jdbc:mysql://localhost:3306/java2601" +
"?serverTimezone=UTC&characterEncoding=utf8";
String sql = "insert into employee(" +
"first_name, salary, department_id" +
") values(?, ?, ?)";
try (Connection connection = DriverManager.getConnection(
url,
"root",
"root"
); PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "张三");
statement.setDouble(2, 8000);
statement.setInt(3, 5001);
int affectedRows = statement.executeUpdate();
System.out.println("受影响行数:" + affectedRows);
} catch (SQLException e) {
e.printStackTrace();
}
}
}执行增、删、改语句
executeUpdate() 用于执行 INSERT、UPDATE、DELETE,返回受影响的行数。
新增数据
String sql = "insert into employee(" +
"first_name, job_id, salary, department_id" +
") values(?, ?, ?, ?)";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "Rose");
statement.setString(2, "程序员");
statement.setDouble(3, 9000);
statement.setInt(4, 5001);
statement.executeUpdate();
}修改数据
String sql = "update employee " +
"set first_name = ?, job_id = ?, salary = ?, department_id = ? " +
"where employee_id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "Jack");
statement.setString(2, "项目经理");
statement.setDouble(3, 12000);
statement.setInt(4, 5002);
statement.setInt(5, 101);
statement.executeUpdate();
}删除数据
String sql = "delete from employee where employee_id = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, 101);
statement.executeUpdate();
}
使用 ResultSet 处理查询结果
调用 executeQuery() 执行查询语句,会返回 ResultSet 对象。
ResultSet 可以理解为带有游标的二维结果集。初始游标位于第一行之前,每次调用 next() 后向下移动一行。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class QueryEmployee {
public static void main(String[] args) {
String url =
"jdbc:mysql://localhost:3306/java2601" +
"?serverTimezone=UTC&characterEncoding=utf8";
String sql = "select " +
"employee_id as eid, first_name, job_id, salary " +
"from employee";
try (Connection connection = DriverManager.getConnection(
url,
"root",
"root"
); PreparedStatement statement = connection.prepareStatement(sql);
ResultSet resultSet = statement.executeQuery()) {
while (resultSet.next()) {
int employeeId = resultSet.getInt("eid");
String firstName = resultSet.getString("first_name");
String jobId = resultSet.getString("job_id");
double salary = resultSet.getDouble("salary");
System.out.println(
employeeId + "\t" +
firstName + "\t" +
jobId + "\t" +
salary
);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}getXxx() 可以通过列序号或列名获取数据。列序号从 1 开始。使用列名或别名通常更清晰。
对于可能为 NULL 的数值列,getInt()、getDouble() 等方法会返回基本类型默认值。需要区分数据库中的 NULL 时,可以调用 wasNull(),或使用 getObject() 获取包装类型。
SQL 注入问题
下面这种字符串拼接方式存在 SQL 注入风险:
String sql = "select * from db_users " +
"where username = '" + username + "' " +
"and password = '" + password + "'";
用户输入会直接成为 SQL 语法的一部分。攻击者可能构造特殊输入改变原查询语义。
使用 PreparedStatement
PreparedStatement 使用占位符表示参数值,并通过类型安全的 setXxx() 方法设置参数。参数值不会被当作 SQL 结构解析,因此能够有效避免大多数 SQL 注入问题。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;
public class LoginTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
String sql = "select id from db_users " +
"where username = ? and password = ?";
try (Connection connection = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/java2601?serverTimezone=UTC&characterEncoding=utf8",
"root",
"root"
); PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, username);
statement.setString(2, password);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}示例仅用于说明 JDBC 查询。真实系统不应在数据库中保存明文密码,应保存经过可靠密码哈希算法处理的结果,并使用随机盐。
Statement 与 PreparedStatement 的区别
| 对比项 | Statement | PreparedStatement |
|---|---|---|
| SQL 参数 | 通常通过字符串拼接 | 使用 ? 占位符 |
| SQL 注入风险 | 较高 | 参数值与 SQL 结构分离 |
| 可读性 | 动态拼接较复杂 | 参数位置清晰 |
| 重复执行 | 每次处理完整 SQL | 驱动或数据库可能复用执行计划 |
| 类型设置 | 依赖字符串格式 | 使用 setString()、setInt() 等方法 |
业务代码中应优先使用 PreparedStatement。表名、列名、排序方向等 SQL 结构不能使用占位符,必须通过可信白名单选择。
JDBC 常用 API
Connection
Connection 表示一次数据库连接。常用方法包括:
| 方法 | 作用 |
|---|---|
prepareStatement() | 创建预编译 SQL 对象 |
setAutoCommit(false) | 关闭自动提交,开始手动控制事务 |
commit() | 提交事务 |
rollback() | 回滚事务 |
setSavepoint() | 创建保存点 |
rollback(Savepoint) | 回滚到指定保存点 |
getMetaData() | 获取数据库元数据 |
close() | 关闭连接 |
多条 SQL 要组成同一个本地事务,必须在同一个 Connection 中执行。
PreparedStatement
常用方法包括 setString()、setInt()、setObject()、executeUpdate() 和 executeQuery()。
参数序号从 1 开始。
ResultSet
常用方法包括 next()、getString()、getInt()、getObject() 和 wasNull()。
JDBC 事务控制
下面的示例在同一个事务中删除两条员工记录。任一操作失败时,全部回滚。
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TransactionTest {
public static void deleteEmployees(
Connection connection,
int firstId,
int secondId
) throws SQLException {
String sql = "delete from employee where employee_id = ?";
boolean originalAutoCommit = connection.getAutoCommit();
try {
connection.setAutoCommit(false);
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, firstId);
statement.executeUpdate();
statement.setInt(1, secondId);
statement.executeUpdate();
}
connection.commit();
} catch (SQLException e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(originalAutoCommit);
}
}
}使用连接池时,归还连接前应恢复连接状态,否则后续借用同一物理连接的代码可能受到影响。
保存点
保存点允许事务回滚到中间状态。
import java.sql.Connection;
import java.sql.Savepoint;
import java.sql.SQLException;
public class SavepointTest {
public static void execute(Connection connection) throws SQLException {
connection.setAutoCommit(false);
Savepoint savepoint = null;
try {
executeFirstSql(connection);
savepoint = connection.setSavepoint("after_first_sql");
executeSecondSql(connection);
connection.commit();
} catch (SQLException e) {
if (savepoint != null) {
connection.rollback(savepoint);
connection.commit();
} else {
connection.rollback();
}
throw e;
}
}
private static void executeFirstSql(Connection connection) {
}
private static void executeSecondSql(Connection connection) {
}
}数据库元数据
通过 DatabaseMetaData 可以获取数据库产品名称、版本、驱动名称和表结构等信息。
import java.sql.DatabaseMetaData;
DatabaseMetaData metadata = connection.getMetaData();
System.out.println(metadata.getDatabaseProductName());
System.out.println(metadata.getDatabaseProductVersion());
System.out.println(metadata.getDriverName());
结果集元数据
通过 ResultSetMetaData 可以获得结果集列数、列名、列类型等信息。
import java.sql.ResultSetMetaData;
ResultSetMetaData metadata = resultSet.getMetaData();
int columnCount = metadata.getColumnCount();
for (int index = 1; index <= columnCount; index++) {
System.out.println(metadata.getColumnLabel(index));
System.out.println(metadata.getColumnTypeName(index));
}JDBC 封装
封装连接创建过程
package com.hyxy.dao;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public final class DBUtil {
private static final String URL =
"jdbc:mysql://localhost:3306/java2601" +
"?serverTimezone=UTC&characterEncoding=utf8";
private static final String USERNAME = "root";
private static final String PASSWORD = "root";
private DBUtil() {
}
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError(e);
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(URL, USERNAME, PASSWORD);
}
}实际项目通常使用数据库连接池,而不是每次直接通过 DriverManager 创建物理连接。
DAO 与 VO
DAO 是 Data Access Object 的缩写,用于封装数据库访问逻辑。业务层通过 DAO 方法完成增、删、改、查,避免在业务代码中散落 SQL。
VO 是 Value Object 的缩写。在当前课程示例中,VO 用于保存数据库记录,字段通常与表结构相对应。实际项目也常使用 Entity、POJO 或 DTO 等名称,但含义并不完全相同。
Apache Commons DbUtils
DbUtils 是对 JDBC 的轻量封装。核心类 QueryRunner 可以简化参数绑定、结果集处理和资源管理。
使用外部连接创建 QueryRunner 时,连接仍由调用者负责关闭。
新增数据
import org.apache.commons.dbutils.QueryRunner;
import java.sql.Connection;
import java.sql.SQLException;
public class DbUtilsInsertTest {
public static void main(String[] args) {
QueryRunner runner = new QueryRunner();
String sql = "insert into employee(first_name, job_id) values(?, ?)";
try (Connection connection = DBUtil.getConnection()) {
int affectedRows = runner.update(
connection,
sql,
"Smith",
"财务总监"
);
System.out.println("受影响行数:" + affectedRows);
} catch (SQLException e) {
e.printStackTrace();
}
}
}获取自增主键
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import java.sql.Connection;
import java.sql.SQLException;
public class DbUtilsPrimaryKeyTest {
public static void main(String[] args) {
QueryRunner runner = new QueryRunner();
String sql = "insert into employee(first_name, job_id) values(?, ?)";
try (Connection connection = DBUtil.getConnection()) {
Number primaryKey = runner.insert(
connection,
sql,
new ScalarHandler<>(),
"Smith2",
"财务总监2"
);
System.out.println(primaryKey);
} catch (SQLException e) {
e.printStackTrace();
}
}
}驱动返回的主键数值具体类型可能不同,使用 Number 通常比固定为 BigInteger 更稳妥。
查询多条记录
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
public class DbUtilsListTest {
public static void main(String[] args) {
QueryRunner runner = new QueryRunner();
String sql = "select * from employee";
try (Connection connection = DBUtil.getConnection()) {
List<Employee> employees = runner.query(
connection,
sql,
new BeanListHandler<>(Employee.class)
);
for (Employee employee : employees) {
System.out.println(employee);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}JavaBean 属性名需要与查询结果的列标签匹配。列名不一致时,可以在 SQL 中使用别名。
select employee_id as employeeId,
first_name as firstName,
department_id as departmentId
from employee;
按条件查询
String sql = "select * from employee " +
"where department_id = ? and salary >= ?";
try (Connection connection = DBUtil.getConnection()) {
List<Employee> employees = runner.query(
connection,
sql,
new BeanListHandler<>(Employee.class),
5001,
3000
);
}按主键查询
import org.apache.commons.dbutils.handlers.BeanHandler;
String sql = "select * from employee where employee_id = ?";
try (Connection connection = DBUtil.getConnection()) {
Employee employee = runner.query(
connection,
sql,
new BeanHandler<>(Employee.class),
101
);
System.out.println(employee);
}用户管理综合示例
创建用户表
drop table if exists t_user;
create table t_user (
id int not null auto_increment,
username varchar(255) null,
pwd varchar(255) null,
email varchar(255) null,
primary key (id)
) engine = InnoDB
default character set = utf8mb4;
insert into t_user(username, pwd, email)
values('Jerry', '888888', 'jerry@126.com');创建用户实体类
package com.hyxy.vo;
public class User {
private int id;
private String username;
private String pwd;
private String email;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
'}';
}
}toString() 中不建议输出密码。
创建用户 DAO
package com.hyxy.dao;
import com.hyxy.vo.User;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
public class UserDao {
private final QueryRunner runner = new QueryRunner();
public int register(User user) throws SQLException {
String sql = "insert into t_user(username, pwd, email) values(?, ?, ?)";
try (Connection connection = DBUtil.getConnection()) {
return runner.update(
connection,
sql,
user.getUsername(),
user.getPwd(),
user.getEmail()
);
}
}
public User login(String username, String password) throws SQLException {
String sql = "select id, username, email " +
"from t_user where username = ? and pwd = ?";
try (Connection connection = DBUtil.getConnection()) {
return runner.query(
connection,
sql,
new BeanHandler<>(User.class),
username,
password
);
}
}
public int update(User user) throws SQLException {
String sql = "update t_user " +
"set username = ?, pwd = ?, email = ? where id = ?";
try (Connection connection = DBUtil.getConnection()) {
return runner.update(
connection,
sql,
user.getUsername(),
user.getPwd(),
user.getEmail(),
user.getId()
);
}
}
public int deleteByUsername(String username) throws SQLException {
String sql = "delete from t_user where username = ?";
try (Connection connection = DBUtil.getConnection()) {
return runner.update(connection, sql, username);
}
}
public List<User> findAll() throws SQLException {
String sql = "select id, username, email from t_user";
try (Connection connection = DBUtil.getConnection()) {
return runner.query(
connection,
sql,
new BeanListHandler<>(User.class)
);
}
}
}示例注意事项
- 交互层不应直接拼接 SQL。
- 数据访问逻辑应放在 DAO 中,业务规则应放在 Service 中。
- 数据库密码和用户密码都不应硬编码。
- 登录功能应使用密码哈希,而不是明文比较。
- 每个 JDBC 资源都必须正确关闭。
- 多条相关更新需要通过事务保证一致性。
喜欢的话,留下你的评论吧~