JDBC

发布于 2026-07-29 10:54 更新于 2026-07-29 10:54 3151 字 16 min read ... 访问量

本文介绍了 JDBC 的基本概念、使用流程和最佳实践。JDBC 是 Java 连接数据库的标准接口,通过驱动程序实现对不同数据库的访问,支持连接管理、SQL 操作、事务控制和元数据获取。文章详细说明了如何连接 MySQL、使用 PreparedStatement 防止 SQL 注入、处理查询结果、管理事务以及资源关闭,并强调了代码安全性和可维护性,如避免硬编码密码、使用 DAO 模式和连接池、正确处理主键和结果集等。

JDBC

JDBC 概述

JDBC 是 Java 提供的一套数据库访问规范,主要位于 java.sqljavax.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);

关闭资源

ConnectionStatementResultSet 都属于需要关闭的资源。推荐使用 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() 用于执行 INSERTUPDATEDELETE,返回受影响的行数。

新增数据

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 查询。真实系统不应在数据库中保存明文密码,应保存经过可靠密码哈希算法处理的结果,并使用随机盐。

StatementPreparedStatement 的区别

对比项StatementPreparedStatement
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 资源都必须正确关闭。
  • 多条相关更新需要通过事务保证一致性。

喜欢的话,留下你的评论吧~

... 访问量
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka