Skip to content

Java进阶教程 - 7 JDBC

JDBC(Java Database Connectivity)也就是通过 Java 来访问数据库。

JDBC 是 Java 提供的一套 访问数据库的标准 API,它的作用就是在 Java 程序和数据库之间搭建一座桥梁,让开发者通过统一的接口操作不同的数据库(如 MySQL、PostgreSQL、Oracle 等),而不必关心底层实现。

在这个章节你首先得有一点数据库方面的知识,不过不用紧张,也不用太多,你可以先学习一下 SQL/MySQL基础教程 ,了解一下数据库的基本操作。

首先准备一下数据库,我们这里就使用 MySQL 数据库了。

首先创建数据库和表:

sql
-- 创建数据库
CREATE DATABASE foooor_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 创建一张用户表
CREATE TABLE tb_user (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    username VARCHAR(50) NOT NULL COMMENT '用户名',
    balance INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '余额',
    create_time DATETIME NOT NULL COMMENT '创建时间'
);

-- 准备3条数据
INSERT INTO tb_user (username, balance, create_time) VALUES
('zhangsan', 150, '2025-09-15 10:30:00'),
('lisi', 200, '2025-09-20 14:15:00'),
('wangwu', 5, '2025-09-05 09:25:00');

数据准备好了!下面就来演示一下使用 JDBC 实现数据的 CRUD。

CRUD 就是数据的增删改查:

C - Create(创建)

R - Read(读取)或 Retrieve(检索)

U - Update(更新)

D - Delete(删除)


7.1 使用JDBC查询数据库

下面从0开始来实现使用 Java 操作数据库。

首先新建一个 Maven 项目,因为我们会使用到第三方的依赖,使用 Maven 管理比较方便。

1 创建项目

创建一个 Maven 项目:

2 引入MySQL依赖

在项目的 pom.xml 中添加 MySQL 的驱动 jar 包:

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.foooor</groupId>
    <artifactId>hello-jdbc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 引入mysql驱动依赖,用于连接mysql数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
    </dependencies>

</project>
  • 添加完成依赖,注意,要右键 pom.xml --> Maven --> Reload project 一下,下载依赖。

Java 定义了统一的接口(JDBC),不同的数据库厂商会根据标准实现 JDBC 的接口,也就是驱动 jar 包,这样 Java 就能用统一方式访问不同数据库,而不关心具体的实现。

3 查询数据

下面就开始编写 Java 代码,实现数据的获取。

步骤主要分为以下几步:

  1. 加载驱动(JDBC 4.0+ 可以省略)
  2. 建立连接
  3. 执行 SQL
  4. 处理结果
  5. 关闭资源

首先创建一个 Java 类,我这里就叫 JdbcTest.java

java
package com.foooor.hellojdbc;

import java.sql.*;
import java.util.Date;  // 注意,不是引入java.sql.Date

public class JdbcTest {

    // 数据库连接信息
    private static final String URL = "jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        // 数据库连接对象
        Connection conn = null;
        // SQL 语句执行对象
        PreparedStatement pstmt = null;
        // 查询结果集对象
        ResultSet rs = null;

        try {
            // 1. 注册 MySQL 驱动(可选,JDBC 4.0+ 可以省略,现在基本不用了)
            Class.forName("com.mysql.cj.jdbc.Driver");

            // 2. 获取数据库连接
            conn = DriverManager.getConnection(URL, USER, PASSWORD);

            String sql = "SELECT * FROM tb_user";
            // 3. 创建 SQL 语句执行对象(PreparedStatement)
            pstmt = conn.prepareStatement(sql);

            // 4. 执行查询,返回结果集
            rs = pstmt.executeQuery();

            // 5. 遍历结果集
            // rs.next() 会移动到下一行,如果有数据则返回 true
            System.out.println("查询结果:");
            while (rs.next()) {
                int id = rs.getInt("id");          // 获取 id 列的值
                String username = rs.getString("username"); // 获取 username 列的值
                int balance = rs.getInt("balance"); // 获取 balance 列的值
                // 获取 DATETIME 字段,返回 Timestamp
                Timestamp timestamp = rs.getTimestamp("create_time");  // 获取create_time列的值
                Date createTime = timestamp;  // Timestamp是Date的子类,所以可以直接赋值

                System.out.println(id + " | " + username + " | " + balance + " | " + createTime);
            }
        } catch (Exception e) {
            // 捕获 SQL 异常并打印
            e.printStackTrace();
        } finally {
            // 5. 关闭资源(顺序很重要!)
            // 先关 ResultSet → 再关 PreparedStatement → 最后关 Connection
            try {
                if (rs != null) rs.close(); // 关闭结果集
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if (pstmt != null) pstmt.close(); // 关闭执行对象
            } catch (SQLException e) {
                e.printStackTrace();
            }
            try {
                if (conn != null) conn.close(); // 关闭连接
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 按照步骤去实现,获取连接、执行SQL、处理结果、关闭连接,拿到结果 ResultSet 以后,需要逐条获取下一条数据,并依次获取其中的字段信息。

执行结果如下:

查询结果:
1 | zhangsan | 150 | 2025-09-15 18:30:00.0
2 | lisi | 200 | 2025-09-20 22:15:00.0
3 | wangwu | 5 | 2025-09-05 17:25:00.0

上面是比较旧的 Java 语法,使用新的 JDK7 以后的 try-with-resources 语法,可以自动关闭资源。

像下面这样写就可以了:

java
package com.foooor.hellojdbc;

import java.sql.*;
import java.util.Date;  // 注意,不是引入java.sql.Date

public class JdbcTest {

    // 数据库连接信息
    private static final String URL = "jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        String sql = "SELECT * FROM tb_user";

        // try-with-resources 写法
        // 括号中的资源会在 try 代码块执行完毕后,自动调用 close() 方法,避免资源泄漏
        try (
                // 1. 获取数据库连接对象(Connection)
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);

                // 2. 创建 SQL 语句执行对象(PreparedStatement)
                PreparedStatement pstmt = conn.prepareStatement(sql);

                // 3. 执行 SQL 查询
                // executeQuery() 用于执行 SELECT 语句,返回一个结果集对象(ResultSet)
                ResultSet rs = pstmt.executeQuery();
        ) {
            // 4. 遍历结果集
            // rs.next() 表示是否有下一行数据,第一次调用会指向第一行
            while (rs.next()) {
                // 通过列名或列索引获取数据
                int id = rs.getInt("id");          // 获取 id 列的值(整型)
                String username = rs.getString("username"); // 获取 username 列的值(字符串)
                int balance = rs.getInt("balance"); // 获取 balance 列的值(字符串)
                // 获取 DATETIME 字段,返回 Timestamp
                Timestamp timestamp = rs.getTimestamp("create_time");
                Date createTime = timestamp;  // Timestamp是Date的子类,所以可以直接赋值

                System.out.println(id + " | " + username + " | " + balance + " | " + createTime);
            }
        } catch (Exception e) {
            // 捕获并打印 SQL 异常
            e.printStackTrace();
        }
        // 注意:这里不需要写 finally 来关闭资源,因为 try-with-resources 会自动关闭
    }
}

7.2 SQL占位符

1 为什么需要占位符

在上面编写 SQL 的时候,是没有参数的,如果有参数的话,你可能会觉得这样写就好了:

java
String sql = "SELECT * FROM tb_user WHERE username = '" + username + "'";

但是 SQL 拼接可能存在 SQL 注入的问题,例如当用户在前端输入 username 的值为 ' OR '1'='1 时,SQL就变为了:

sql
SELECT * FROM tb_user WHERE username = '' OR '1'='1'

'1'='1' 的条件是永远成立的,那么会查询出所有的数据,如果这是登录验证逻辑(只检查是否有结果),攻击者即可绕过认证,登录成功。


所以这里需要使用 SQL 占位符, ? 就是 SQL 的参数占位符。在 JDBC 里,当 SQL 中存在 ? 时,说明这是一个 预编译的 SQL,执行前需要通过 PreparedStatement 来填充参数。

2 占位符的使用

举个栗子,根据 id 来查询指定的用户:

java
package com.foooor.hellojdbc;

import java.sql.*;
import java.util.Date;  // 注意,不是java.sql.Date

public class JdbcTest {

    // 数据库连接信息
    private static final String URL = "jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        selectUserById(1);
    }

    /**
     * 按 ID 查询单个用户
     */
    public static void selectUserById(int id) {

        String sql = "SELECT * FROM tb_user WHERE id = ?";  // SQL中使用占位符

        try (
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                PreparedStatement pstmt = conn.prepareStatement(sql)
        ) {
            pstmt.setInt(1, id);  // 设置第一个占位符(?)为 id

            // 执行查询
            ResultSet rs = pstmt.executeQuery();

            // 遍历结果集(一般只有一条)
            while (rs.next()) {
                int uid = rs.getInt("id");
                String username = rs.getString("username");
                int balance = rs.getInt("balance");
                Timestamp timestamp = rs.getTimestamp("create_time");
                Date createTime = timestamp;  // Timestamp是Date的子类,所以可以直接赋值

                System.out.println(uid + " | " + username + " | " + balance + " | " + createTime);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  • 在上面的代码中, SQL 中的参数使用 ? 代替,然后使用 PreparedStatement 来设置对应位置的具体参数。这样可以避免 SQL 注入的风险。

7.3 增删改操作

上面演示的是查询操作,下面把增删改操作也演示一下:

java
package com.foooor.hellojdbc;

import java.sql.*;
import java.util.Date;  // 注意,不是java.sql.Date

public class JdbcTest {

    // 数据库连接信息
    private static final String URL = "jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        selectUserById(1);
        insertUser("zhaoliu", 300, new Date());
        updateUser(1, 200);
        deleteUser(1);
        selectAllUsers();
    }

    /**
     * 查询所有用户
     */
    public static void selectAllUsers() {
        // 查询所有用户
        String sql = "SELECT * FROM tb_user";
        try (
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                PreparedStatement pstmt = conn.prepareStatement(sql);
                // 执行查询
                ResultSet rs = pstmt.executeQuery()
        ) {
            // 遍历所有结果
            while (rs.next()) {
                int uid = rs.getInt("id");  // 根据列名获取值
                String username = rs.getString("username");
                int balance = rs.getInt("balance");
                Timestamp timestamp = rs.getTimestamp("create_time");
                Date createTime = timestamp;  // Timestamp是Date的子类,所以可以直接赋值

                System.out.println(uid + " | " + username + " | " + balance + " | " + createTime);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 按 ID 查询单个用户
     */
    public static void selectUserById(int id) {
        String sql = "SELECT * FROM tb_user WHERE id = ?";
        try (
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                PreparedStatement pstmt = conn.prepareStatement(sql)
        ) {
            pstmt.setInt(1, id);  // 设置第一个占位符(?)为 id

            // 执行查询
            ResultSet rs = pstmt.executeQuery();

            // 遍历结果集(一般只有一条)
            while (rs.next()) {
                int uid = rs.getInt("id");  // 根据列名获取值
                String username = rs.getString("username");
                int balance = rs.getInt("balance");
                Timestamp timestamp = rs.getTimestamp("create_time");
                Date createTime = timestamp;  // Timestamp是Date的子类,所以可以直接赋值
                System.out.println(uid + " | " + username + " | " + balance + " | " + createTime);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 新增用户
     */
    public static void insertUser(String username, int balance, Date createTime) {
        // 插入 SQL 语句
        String sql = "INSERT INTO tb_user (username, balance, create_time) VALUES (?, ?, ?)";
        try (
                // 1. 获取数据库连接
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                // 2. 预编译 SQL 语句
                PreparedStatement pstmt = conn.prepareStatement(sql)
        ) {
            // 3. 设置参数
            pstmt.setString(1, username);  // 这里是从1开始不是0
            pstmt.setInt(2, balance);
            pstmt.setTimestamp(3, new Timestamp(createTime.getTime()));

            // 4. 执行 SQL
            int rows = pstmt.executeUpdate();
            System.out.println("插入成功,影响行数:" + rows);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 更新用户
     */
    public static void updateUser(int id, int money) {
        // 根据id更新
        String sql = "UPDATE tb_user SET balance = balance + ? WHERE id = ?";
        try (
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                PreparedStatement pstmt = conn.prepareStatement(sql)
        ) {
            // 设置参数
            pstmt.setInt(1, money);
            pstmt.setInt(2, id);

            int rows = pstmt.executeUpdate();
            System.out.println("更新成功,影响行数:" + rows);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 删除用户
     * @param id 用户ID
     */
    public static void deleteUser(int id) {
        // 删除指定id的用户
        String sql = "DELETE FROM tb_user WHERE id = ?";
        try (
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                PreparedStatement pstmt = conn.prepareStatement(sql)
        ) {
            // 设置参数
            pstmt.setInt(1, id);

            int rows = pstmt.executeUpdate();
            System.out.println("删除成功,影响行数:" + rows);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
  • 其实操作都差不多,准备SQL、执行连接、要么执行查询,要么执行更新(增、删、改)、获取结果、关闭资源(或自动关闭)。

7.4 事务

事务是一组数据库操作的集合,这些操作要么 全部成功,要么 全部失败

例如张三给李四转账100,要张三的账户减去100,李四的账户加上100,这是一组操作,必须全部成功,不能张三账户减去100成功,李四账户加上100失败,这会导致数据错误。

在 JDBC 中,默认情况下每条 SQL 都是一个单独的事务(即自动提交模式,autoCommit=true)。你执行一条更新的 SQL ,它会立即提交到数据库。

所以我们在进行增、删、改操作(查询不需要)的时候,如果涉及到多条 SQL,就需要关闭自动提交,手动开启事务,并手动提交事务。

所以操作需要分为以下步骤:

  1. 关闭事务自动提交;
  2. 手动开启事务;
  3. 执行多条SQL;
  4. 手动提交事务;
  5. 执行出错,回滚事务。

下面使用一个例子演示一下:

我们这里将张三的账户 -100,将李四的账户 +100,如果中间出错,就回滚所有操作。

java
package com.foooor.hellojdbc;

import java.sql.*;
import java.util.Date;  // 注意,不是java.sql.Date

public class JdbcTest {

    // 数据库连接信息
    private static final String URL = "jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        testTransaction();
    }

    public static void testTransaction() {
        Connection conn = null;
        PreparedStatement updateStmt1 = null;
        PreparedStatement updateStmt2 = null;

        try {
            // 1. 获取连接
            conn = DriverManager.getConnection(URL, USER, PASSWORD);

            // 2. 关闭自动提交,开启事务
            conn.setAutoCommit(false);

            // 3. 更新zhangsan的账户,减去100
            String updateSql1 = "UPDATE tb_user SET balance = balance - ? WHERE username = ?";
            updateStmt1 = conn.prepareStatement(updateSql1);
            updateStmt1.setInt(1, 100);
            updateStmt1.setString(2, "zhaoliu");
            int rows1 = updateStmt1.executeUpdate();
            if (rows1 == 0) {
                // 如果没有更新到数据,则抛出异常,会被catch捕获,然后回滚事务
                throw new SQLException("更新张三账户失败!");
            }

            // 4. 更新lisi的账户,加上100
            String updateSql2 = "UPDATE tb_user SET balance = balance + ? WHERE username = ?";
            updateStmt2 = conn.prepareStatement(updateSql2);
            updateStmt2.setInt(1, 100);
            updateStmt2.setString(2, "lisi");
            int rows2 = updateStmt2.executeUpdate();
            if (rows2 == 0) {
                // 如果没有更新到数据,则抛出异常,会被catch捕获,然后回滚事务
                throw new SQLException("更新李四账户失败!");
            }

            // 5. 提交事务
            conn.commit();
            System.out.println("事务提交成功!");

        } catch (Exception e) {
            e.printStackTrace();
            try {
                if (conn != null) {
                    // 6. 出错时回滚
                    conn.rollback();
                    System.out.println("事务已回滚!");
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            try {
                if (conn != null) {
                    // 7. 恢复自动提交
                    conn.setAutoCommit(true);
                }
            } catch (Exception e) {}
            try { if (updateStmt1 != null) updateStmt1.close(); } catch (Exception e) {}
            try { if (updateStmt2 != null) updateStmt2.close(); } catch (Exception e) {}
            try { if (conn != null) conn.close(); } catch (Exception e) {}
        }
    }
}
  • 如果上面有一个步骤出错,就会抛出异常,捕获异常以后,就会回滚事务;
  • 需要注意的点:首先关闭自动提交,执行完成后需要恢复自动提交,同时出现异常的时候,要回滚事务。

7.5 封装工具类

上面在进行增、删、改、查的时候,因为每一次都要进行连接、关闭等操作,每一次操作都要进行很多重复的操作,所以我们可以将一些重复的操作封装成一个工具类。

例如我封装成一个 JdbcHelper.java,代码如下:

java
package com.foooor.hellojdbc;

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

public class JdbcHelper {

    private static final String URL = "jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    static {
        try {
            // 注册驱动(JDBC 4.0+ 可省略)
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 通用查询方法
     * @param sql    SQL语句,支持 ? 占位符
     * @param params 参数列表
     * @return       每行数据为一个 Map
     */
    public static List<Map<String,Object>> query(String sql, List<Object> params) {
        List<Map<String,Object>> list = new ArrayList<>();  // 结果放在List中,每一行数据是一个Map
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            pstmt = conn.prepareStatement(sql);

            // 遍历参数,设置占位符的值
            if (params != null) {
                for (int i = 0; i < params.size(); i++) {
                    pstmt.setObject(i + 1, params.get(i));
                }
            }

            rs = pstmt.executeQuery();
            ResultSetMetaData meta = rs.getMetaData();
            int columnCount = meta.getColumnCount();  // 获取查询到的结果集的列数

            while (rs.next()) {
                Map<String,Object> row = new HashMap<>();  // 每一行数据封装为一个map
                for (int i = 1; i <= columnCount; i++) {
                    row.put(meta.getColumnLabel(i), rs.getObject(i));  // 列名作为键,对应的值作为值
                }
                list.add(row);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rs, pstmt, conn);  // 关闭结果集、语句和连接
        }
        return list;
    }

    /**
     * 通用更新方法(INSERT / UPDATE / DELETE)
     * 自动事务管理:成功提交,失败回滚
     */
    public static int update(String sql, List<Object> params) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        int affectedRows = 0;

        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            // 首先记录一开始的提交状态,后面要恢复,因为一开始的提交模式有可能是自动提交有可能是手动提交,最后恢复为一开始的状态
            boolean originalAutoCommit = conn.getAutoCommit();  
            try {
                conn.setAutoCommit(false);  // 关闭自动提交,开启事务
                pstmt = conn.prepareStatement(sql);

                // 设置占位符的值
                if (params != null) {
                    for (int i = 0; i < params.size(); i++) {
                        pstmt.setObject(i + 1, params.get(i));
                    }
                }

                affectedRows = pstmt.executeUpdate();  // 获取到影响到的行
                conn.commit();  // 提交事务
            } catch (SQLException e) {
                e.printStackTrace();
                if (conn != null) conn.rollback();  // 失败则回滚事务
            } finally {
                if (conn != null) {
                    try {
                        conn.setAutoCommit(originalAutoCommit);  // 恢复一开始的提交状态
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(null, pstmt, conn);  // 关闭语句和连接
        }
        return affectedRows;
    }

    /**
     * 关闭资源
     */
    private static void close(ResultSet rs, Statement stmt, Connection conn) {
        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(); } }
    }
}
  • 在上面的代码中,主要封装了两个方法,一个是查询,一个是更新;
  • 我们将SQL的参数作为方法的参数,通过列表传入,然后设置给 PreparedStatement
  • 查询的结果数据,每一条数据作为一个 Map ,其中每一列的数据是 Map 中一条记录。
  • 你也可以自己封装自己的 JDBC 工具类,或者网上找别人写的。

那么有了上面的工具类,我们查询的时候,可以这么调用了:

java
// 查询
List<Object> params = new ArrayList<Object>();
params.add(2);

// 执行查询
List<Map<String,Object>> users = JdbcHelper.query(
        "SELECT * FROM tb_user WHERE id = ?",
        params
);

// 得到查询结果
for (Map<String,Object> row : users) {
    System.out.println(row);
}

同样,更新、删除、修改也可以调用更新的方法:

java
List<Object> params = new ArrayList<Object>();
params.add("new_name");
params.add(1);

int rows = JdbcHelper.update(
        "UPDATE tb_user SET username = ? WHERE id = ?",
        params
);
System.out.println("影响行数:" + rows);

这样操作就方便和清晰很多了。

当然上面工具类的局限性很大,这里只是做一个演示,你可以根据你的需要进行封装。

7.6 数据库连接池

数据库连接(Connection)是一个非常 重量级资源,建立和释放的代价很大,需要经历很多的步骤,例如:

  1. TCP 连接 → 握手
  2. 用户认证
  3. 分配数据库资源

如果每次 SQL 都去 DriverManager.getConnection(),执行完再 close(),就会频繁创建销毁,性能非常差。

而且可以避免无限制创建数据库连接,防止数据库压力过大。


为了提高资源的利用率,我们可以使用数据库了连接池,主要的原理如下:

  • 预先创建一批连接(多个),放在一个池子里。
  • 应用程序要用连接时,从池子里取;用完后不会 close() ,不会销毁连接,而是归还池子。
  • 下次谁再使用的时候,再从池子中获取一个链接。
  • 这样大大减少了连接的创建/销毁次数,提高性能。

因为同时会有多个线程或用户操作数据库,所以在初始化的时候可以创建多个连接放到连接池中,后面连接不够了,可以再创建新的连接,同时可以设置最大连接数,如果超过最大连接数了,就不会创建新的连接了。同时后面获取连接要看连接池中有没有空闲的连接,如果没有就要等别人释放连接,将连接重新归还到连接池,才能获取到连接。


一般常见连接池有:

  • Druid(阿里开源,功能全面,监控强大)
  • HikariCP(Spring Boot 默认,性能极高)
  • C3P0、DBCP(老牌,但现在较少用)

下面我们就简单介绍一下 Druid 的使用。


1 引入依赖

在项目的 pom.xml 中引入 Druid 的依赖:

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.foooor</groupId>
    <artifactId>hello-jdbc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 引入mysql驱动依赖,用于连接mysql数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <!-- 引入druid依赖,提供数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.27</version>
        </dependency>
    </dependencies>

</project>

2 使用连接池

这里直接在前面编写的工具类 JdbcHelper.java 的基础上进行修改。

操作也很简单,我们只需要修改获取数据库连接的地方就可以了,通过 DruidDataSource 去获取连接,这样数据库连接都交由 DruidDataSource 管理。

java
package com.foooor.hellojdbc;

import com.alibaba.druid.pool.DruidDataSource;
import java.sql.*;
import java.util.*;

public class JdbcHelper {

    private static final DruidDataSource dataSource = new DruidDataSource();

    static {
        // 基础配置
        dataSource.setUrl("jdbc:mysql://localhost:3306/foooor_db?useSSL=false&serverTimezone=UTC");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");

        // 连接池配置(可调节)
        dataSource.setInitialSize(5);     // 初始化连接数
        dataSource.setMaxActive(20);      // 最大活跃连接数
        dataSource.setMinIdle(5);         // 最小空闲连接数
        dataSource.setMaxWait(60000);     // 最大等待时间(毫秒)
    }

    /**
     * 通用查询方法
     * @param sql    SQL语句,支持 ? 占位符
     * @param params 参数列表
     * @return       每行数据为一个 Map
     */
    public static List<Map<String,Object>> query(String sql, List<Object> params) {
        List<Map<String,Object>> list = new ArrayList<>();
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            conn = dataSource.getConnection();  // 从连接池获取连接
            pstmt = conn.prepareStatement(sql);

            if (params != null) {
                for (int i = 0; i < params.size(); i++) {
                    pstmt.setObject(i + 1, params.get(i));
                }
            }

            rs = pstmt.executeQuery();
            ResultSetMetaData meta = rs.getMetaData();
            int columnCount = meta.getColumnCount();

            while (rs.next()) {
                Map<String,Object> row = new HashMap<>();
                for (int i = 1; i <= columnCount; i++) {
                    row.put(meta.getColumnLabel(i), rs.getObject(i));
                }
                list.add(row);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rs, pstmt, conn);
        }
        return list;
    }

    /**
     * 通用更新方法(INSERT / UPDATE / DELETE)
     * 自动事务管理:成功提交,失败回滚
     */
    public static int update(String sql, List<Object> params) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        int affectedRows = 0;

        try {
            conn = dataSource.getConnection();
            boolean originalAutoCommit = conn.getAutoCommit();
            try {
                conn.setAutoCommit(false);
                pstmt = conn.prepareStatement(sql);

                if (params != null) {
                    for (int i = 0; i < params.size(); i++) {
                        pstmt.setObject(i + 1, params.get(i));
                    }
                }

                affectedRows = pstmt.executeUpdate();
                conn.commit();
            } catch (SQLException e) {
                e.printStackTrace();
                if (conn != null) conn.rollback();
            } finally {
                if (conn != null) {
                    try {
                        conn.setAutoCommit(originalAutoCommit);  // 恢复原始事务模式
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(null, pstmt, conn);
        }
        return affectedRows;
    }

    /**
     * 关闭资源(归还连接到连接池)
     */
    private static void close(ResultSet rs, Statement stmt, Connection conn) {
        if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } }
        if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } }
        // 注意:conn.close() 底层不会关闭连接,而是归还连接到连接池
        if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } }
    }
}
  • 使用是很简单的,就是一开始创建了一个 DruidDataSource 数据源,后面获取数据库连接的时候,通过这个数据源获取就可以了。
  • 其他的代码和之前是一样的,不用动。
  • 在最后关闭连接的时候,底层不是真的关闭了连接,而是将连接归还给连接池。

通过连接池,可以提高数据库的访问效率,减少开销,还可以帮我们管理连接,控制并发,避免数据库压力过大。


上面操作 JDBC 操作数据库,是比较基础和原始的方式,每次查询都要手动写 SQL、创建 ConnectionPreparedStatementResultSet,用完还要自己关闭资源。查询结果要手动解析,把每一行封装成对象或 Map,操作都比较麻烦。

在现在的项目中,我们一般会使用一些第三方的框架来操作数据库,例如 Spring JdbcTemplateMyBatisHibernate等(现在用的少了),框架会帮我们完成数据的封装,简化我们的操作,但是底层还是通过 JDBC来实现的。

内容未完......