JDBC进阶篇:拓展功能与连接池运用详解


精选专栏链接 🔗


欢迎订阅,点赞+关注,每日精进1%,与百万开发者共攀技术珠峰

更多内容持续更新中!希望能给大家带来帮助~ 😀😀😀


JDBC进阶篇:拓展功能与连接池运用详解


1,什么是ORM思想

ORM(Object Relational Mapping,对象关系映射)是一种在编程领域广泛应用的核心技术思想,其本质是搭建起面向对象编程范式与关系型数据库之间的桥梁,实现两者概念的深度融合

对象关系映射对应关系:

  • 数据库的每张表对应一个类,内部封装表结构与操作;
  • 每行数据映射为类的一个对象实例,承载具体数据;
  • 每列则对应对象的属性,用于存取数据;

为了代码整洁,创建一个新的包,包内创建与已有数据库 t_emp 表对应的 Employee 类(数据库脚本见 基础篇内容(待修改)


Employee类的具体代码如下:

  • 可通过 Alt+Insert 快速插入,get()、set()、toString();
java 复制代码
package com.hpu.advanced.pojo;

/**
 * 此处我们设置的类名对应的是数据库表的t_后面的单词全写
 */
public class Employee {

    private Integer empId;     // 对应emp_id

    private String empName;      // 对应emp_name

    private Double empSalary;   // 对应emp_Salary

    private Integer empAge;   // 对应empAge

    public Employee() {     // 无参构造
    }

    public Employee(Integer empId, String empName, Double empSalary, Integer empAge) {  //全参构造
        this.empId = empId;
        this.empName = empName;
        this.empSalary = empSalary;
        this.empAge = empAge;
    }

    // get和set方法
    public Integer getEmpId() {
        return empId;
    }

    public void setEmpId(Integer empId) {
        this.empId = empId;
    }

    public String getEmpName() {
        return empName;
    }

    public void setEmpName(String empName) {
        this.empName = empName;
    }

    public Double getEmpSalary() {
        return empSalary;
    }

    public void setEmpSalary(Double empSalary) {
        this.empSalary = empSalary;
    }

    public Integer getEmpAge() {
        return empAge;
    }

    public void setEmpAge(Integer empAge) {
        this.empAge = empAge;
    }

    // toString方法
    @Override
    public String toString() {
        return "Employee{" +
                "empId=" + empId +
                ", empName='" + empName + '\'' +
                ", empSalary=" + empSalary +
                ", empAge=" + empAge +
                '}';
    }
}

2,ORM思想封装对象

接下来我们演示基于 JDBC 和 ORM 思想实现对象的封装。

注意:和 MyBatis、JPA 之类的自动ORM框架不同,为了更详细阐述流程,如下的演示是手动ORM

首先创建测试类 JDBCAdvanced


2.1,ORM思想封装单个对象

封装单个对象,测试类代码如下:

java 复制代码
package com.hpu.advanced;

import com.hpu.advanced.pojo.Employee;
import org.junit.Test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class JDBCAdvanced {

    @Test
    public void testORM() throws Exception {
        //1,注册驱动(可省略)
        Class.forName("com.mysql.cj.jdbc.Driver");

        //2.获取数据库连接
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/JDBC","your_name","your_password");

        //3.创建PreparedStatement对象,并预编译SQL语句
        PreparedStatement preparedStatement = connection.prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp where emp_id = ?");

        // 4.为占位符赋值,执行SQL语句,获取结果
        preparedStatement.setInt(1,1);  // 注意第一个1是占位符的位置,第二个1是赋值给占位符的值
        ResultSet resultSet = preparedStatement.executeQuery();

        // 5,ORM思想封装单个对象
        Employee employee = null;  //此处设置为null,可以保证查询不到结果就不创建对象,避免资源浪费
        while (resultSet.next()){
            employee = new Employee(); // 查到对象了再创建对象
            int empId = resultSet.getInt("emp_id");
            String empName = resultSet.getString("emp_name");
            Double empSalary = resultSet.getDouble("emp_salary");
            int empAge = resultSet.getInt("emp_age");
            // 为对象属性赋值
            employee.setEmpId(empId);
            employee.setEmpName(empName);
            employee.setEmpSalary(empSalary);
            employee.setEmpAge(empAge);
        }
        System.out.println(employee);

        // 6.释放资源
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
}

运行结果如下:

此时输出的不再是零散数据,而是封装好的一个Java对象,这就是一个 ORM 思想的具体落地。


2.2,ORM思想封装多个对象

封装多个对象,测试类代码如下:

java 复制代码
@Test
public void testORMList() throws Exception {
    //1,注册驱动(可省略)
    Class.forName("com.mysql.cj.jdbc.Driver");

    //2.获取数据库连接
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/JDBC","your_name","your_password");

    //3.创建PreparedStatement对象,并预编译SQL语句
    PreparedStatement preparedStatement = connection.prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp");

    // 4.执行SQL语句,获取结果
    ResultSet resultSet = preparedStatement.executeQuery();

    // 5,ORM思想封装多个对象
    Employee employee = null;  //此处设置为null,可以保证查询不到结果就不创建对象,避免资源浪费
    List<Employee> employeeList = new ArrayList<>();
    while (resultSet.next()){
        employee = new Employee(); // 查到对象了再创建对象

        int empId = resultSet.getInt("emp_id");
        String empName = resultSet.getString("emp_name");
        Double empSalary = resultSet.getDouble("emp_salary");
        int empAge = resultSet.getInt("emp_age");

        // 为对象属性赋值
        employee.setEmpId(empId);
        employee.setEmpName(empName);
        employee.setEmpSalary(empSalary);
        employee.setEmpAge(empAge);

        // 将每次循环封装的一行对象添加到集合中
        employeeList.add(employee);
    }

    // 6,处理结果,遍历集合
    for (Employee emp : employeeList){
        System.out.println(emp);
    }

    // 7.释放资源
    resultSet.close();
    preparedStatement.close();
    connection.close();
}

运行结果如下:


3,主键回显

回想一个实际场景:我们在注册成为某个APP的用户之后,经常会跳转到另外一个完善资料界面(比如下图:选择性别和兴趣)。

显然此场景中,从技术角度分析:

  • 注册是新增一条用户记录;
  • 完善资料是对这条新增的用户的记录进行修改操作;

完善资料需要用刚才新增的用户的主键ID作为后续修改的 where 子句条件,但是新增用户操作返回的是受影响的行数,无法得知当前新增数据的主键值; 在Java程序中获取数据库中插入新数据后的主键值,并赋值给Java对象,此操作即为主键回显


主键回显代码实现:

要想完成主键回显操作,需要:

  • 创建preparedStatement对象时,传入需要主键回显参数Statement.RETURN_GENERATED_KEYS;此参数是 Statement接口里定义的一个常量值;
  • 获取新增的主键时,需要调用getGeneratedKeys()方法,返回的是ResultSet类型的结果集对象;
java 复制代码
@Test
public void testReturnPK() throws Exception {
    //1.注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");

    //2.获取数据库连接
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/JDBC", "your_name","your_password");

    //3.创建preparedStatement对象,传入需要主键回显参数Statement.RETURN_GENERATED_KEYS
    String sql = "insert into t_emp (emp_name, emp_salary, emp_age) values  (?, ?,?)";
    PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

    //4.创建对象,填充对象的属性值
    Employee employee = new Employee(null,"rose",666.66,28);
    preparedStatement.setString(1, employee.getEmpName());
    preparedStatement.setDouble(2, employee.getEmpSalary());
    preparedStatement.setDouble(3, employee.getEmpAge());
    int result = preparedStatement.executeUpdate();

    //5.处理结果
    ResultSet resultSet = null;   // 方便资源关闭(涉及到ResultSet时会关闭,不涉及无需关闭)
    if(result>0){
        System.out.println("添加成功");

        // 添加成功之后才拿主键值:获取生成的主键列值,返回的是resultSet,在结果集中获取主键列值
        resultSet = preparedStatement.getGeneratedKeys();
        //获取生成的主键列值,返回的是resultSet,在结果集中获取主键列值
        if (resultSet.next()){
            int empId = resultSet.getInt(1);  // 由于返回的数据是单行单列,且没有列名,所以只能用1表示取第一个参数
            employee.setEmpId(empId);
        }
        System.out.println(employee);   // 输出查看
    }else{
        System.out.println("添加失败");
    }

    //7.释放资源(先开后关原则)
    if (resultSet != null){
        resultSet.close();    // 不为空时才需要关闭,加入if判断防止空指针
    }
    preparedStatement.close();
    connection.close();

}

运行结果如下,成功获取到了新增记录的 ID 信息:

此时查询数据库就会发现数据新增成功:


4,批量操作

前面我们演示的向数据库中添加数据基本上都是添加一行数据。如果要先数据库中插入很多行数据,应该如何操作呢?

如果还是采用之前的方法,代码如下:

java 复制代码
@Test
public void testMoreInsert() throws Exception {
    //1.注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");

    //2.获取连接
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/JDBC", "your_name","your_password");

    //3.编写SQL语句
    String sql = "insert into t_emp (emp_name,emp_salary,emp_age) values (?,?,?)";

    //4.创建预编译的PreparedStatement,传入SQL语句
    PreparedStatement preparedStatement = connection.prepareStatement(sql);

    long start = System.currentTimeMillis();  //获取当前行代码执行的时间。毫秒值
    for(int i = 0; i<10000; i++){  // 循环插入一万条数据
        //5.为占位符赋值
        preparedStatement.setString(1, "marry"+i);
        preparedStatement.setDouble(2, 100.0+i);
        preparedStatement.setInt(3, 20+i);

        preparedStatement.executeUpdate();  // 实际上执行了一万次Insert,和数据库交互了一万次
    }

    long end = System.currentTimeMillis();

    System.out.println("消耗时间:"+(end - start));

    preparedStatement.close();
    connection.close();
}

运行结果如下:

上述代码执行了一万次的 Insert 语句,相当于和数据库交互一万次,耗时 13732 毫秒,显然很不合理,因为实际上交互一次即可实现插入,交互很多次反而增加了网络和资源的开销。

在此情况下可以使用JDBC的批量操作,JDBC 批量操作可以一次性提交多条 SQL 语句至数据库执行,可以有效避免逐条插入的频繁网络交互与事务开销,显著提升数据插入效率。

在进行批量操作时:

  • 须在连接数据库的URL后面追加?rewriteBatchedStatements=true,表示允许批量操作;
  • 新增SQL必须用values,不能 使用 value;
  • SQL语句的最后不要追加;结束;
  • 每次循环需要调用addBatch()方法,将SQL语句进行批量添加操作;
  • 最后统一执行批量操作,调用executeBatch();

Java 代码实现如下:

java 复制代码
@Test
public void testBatch() throws Exception {
    //1.注册驱动
    Class.forName("com.mysql.cj.jdbc.Driver");

    //2.获取连接(URl需要追加参数)
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/JDBC?rewriteBatchedStatements=true", "your_name","your_password");

    //3.编写SQL语句
        /*
            注意:1、必须在连接数据库的URL后面追加?rewriteBatchedStatements=true,允许批量操作
                2、新增SQL必须用values。且语句最后不要追加;结束
                3、调用addBatch()方法,将SQL语句进行批量添加操作
                4、统一执行批量操作,调用executeBatch()
         */
    String sql = "insert into t_emp (emp_name,emp_salary,emp_age) values (?,?,?)";

    //4.创建预编译的PreparedStatement,传入SQL语句
    PreparedStatement preparedStatement = connection.prepareStatement(sql);

    //获取当前行代码执行的时间。毫秒值
    long start = System.currentTimeMillis();
    for(int i = 0; i<10000; i++){
        //5.为占位符赋值
        preparedStatement.setString(1, "marry"+i);
        preparedStatement.setDouble(2, 100.0+i);
        preparedStatement.setInt(3, 20+i);

        preparedStatement.addBatch();
    }

    //执行批量操作
    preparedStatement.executeBatch();

    long end = System.currentTimeMillis();

    System.out.println("消耗时间:"+(end - start));

    preparedStatement.close();
    connection.close();
}

运行结果如下:

显然批量操作仅和数据库交互一次,使用极少的时间即可完成一万条数据的插入,实现了更强的性能。


5,连接池

目前我们每次操作数据库都要获取新连接,使用完毕后就close释放,频繁的创建和销毁造成资源浪费。此外连接的数量如果无法把控,对服务器来说压力巨大。因此我们引入了连接池。

连接池就是数据库连接对象的缓冲区,通过配置,由连接池负责创建连接、管理连接、释放连接等操作。

  • 预先创建数据库连接并将其放入连接池,用户在请求时,通过池直接获取连接,使用完毕后,将连接放回池中,避免了频繁的创建和销毁,同时解决了创建的效率;

  • 当池中无连接可用,且未达到容量上限时,连接池会新建连接;

  • 池中连接达到上限,用户请求会等待,可以设置超时时间;

类比一个具体的例子:

中国移动大概有10亿多用户,如果10亿个用户同时打人工客服电话,并不需要给10亿个用户一人配一个客服。假设只有10万个客服,能同时接听10万个电话,如果此时有超过10万的电话打来,后面打来的人就需要排队等候。当某些客服完成了一次服务之后,就会继续接听下一个用户的电话。这就是一个类似连接池的例子。


5.1,常见数据库连接池

JDBC 的数据库连接池使用 javax.sql.DataSource接口进行规范,所有的第三方连接池都实现此接口,自行添加具体实现。也就是说,所有连接池获取连接的和回收连接方法都一样,不同的只有性能和扩展功能。

  • C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以;
  • DBCP 是Apache提供的数据库连接池,速度相对C3P0较快,但自身存在一些BUG;
  • Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能, 稳定性较c3p0差一点;
  • Druid 是阿里提供的数据库连接池,是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池,性能、扩展性、易用性都更好,功能丰富
  • Hikari(ひかり[shi ga li]) 取自日语,是光的意思,是SpringBoot2.x之后内置的一款连接池,基于 BoneCP (已经放弃维护)做了不少的改进和优化,口号是快速、简单、可靠。

其中 Druid 是国内用的最多的一款数据库连接池,扩展性较 Hikari 更好;Hikari 是效率最高的一款连接池,性能远超 Druid 因此,如果想扩展性能建议使用Druid,如果追求极致的效率建议使用 Hikari。下面我们重点重点学习这两款连接池的使用,为了更好的演示,我们需要把如下相关的 jar 包引入到项目的 lib 目录下。

bash 复制代码
资源链接: https://pan.baidu.com/s/1oEwmBXmFBQRK26YRMpbMhQ?pwd=yyds 提取码: yyds

5.2,Druid连接池的使用

如果采用硬编码方式实现(可只做了解,不推荐使用),Java代码示例如下:

java 复制代码
@Test
public void testHardCodeDruid() throws SQLException {
        /*
            硬编码:将连接池的配置信息和Java代码耦合在一起。
            1、创建DruidDataSource连接池对象。
            2、设置连接池的配置信息
            3、通过连接池获取连接对象
            4、回收连接【注意此处不是释放连接,而是将连接归还给连接池,给其他线程进行复用】
         */

    //1.创建DruidDataSource连接池对象。
    DruidDataSource druidDataSource = new DruidDataSource();

    //2.设置连接池的配置信息
    //2.1 必须设置的配置
    druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    druidDataSource.setUrl("jdbc:mysql://localhost:3306/JDBC");
    druidDataSource.setUsername("your_username");
    druidDataSource.setPassword("your_password");

    //2.2 非必须设置的配置
    druidDataSource.setInitialSize(10);   // 初始化连接的数量
    druidDataSource.setMaxActive(20);   // 最大连接数

    //3.通过连接池获取连接对象
    Connection connection = druidDataSource.getConnection();
    System.out.println(connection);

    //4.基于connection进行CRUD

    //5.回收连接(注意此处不是释放连接,而是将连接归还给连接池,给其他线程进行复用)
    connection.close();
}

运行结果如下:
硬编码方式中Java代码和连接池的配置等代码,耦合在了一起,不利于代码的维护和更新。因此更推荐使用软编码方式,将连接池的配置提取出来放到专门的配置文件内,将配置文件和Java代码解耦,以便于更新和维护代码。

如果采用软编码方式实现(推荐),演示如下:

① 首先在项目目录下创建resources文件夹,并标识该文件夹为资源目录,创建 db.properties 配置文件,将数据库连接池配置信息定义在该文件中。


② 在新建好的db.properties文件内写入配置信息:

bash 复制代码
driverClassName =com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/atguigu
username=your_username
password=your_password
initialSize=10
maxActive=20

③ 编写Java测试代码如下:

java 复制代码
@Test
public void testResourcesDruid() throws Exception {

    //1.创建Properties集合,用于存储外部配置文件的key和value值。
    Properties properties = new Properties();

    //2.读取外部配置文件,获取输入流,加载到Properties集合里。
    InputStream inputStream = DruidTest.class.getClassLoader().getResourceAsStream("db.properties");  // 加载类的时候读取并加载配置问价
    properties.load(inputStream);

    //3.基于Properties集合构建DruidDataSource连接池
    DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);  // 通过Druid工厂类基于配置信息创建连接池,此处为多态方式接收值

    //4.通过连接池获取连接对象
    Connection connection = dataSource.getConnection();
    System.out.println(connection);

    //5.CRUD

    //6.回收连接
    connection.close();
}

运行结果如下,成功获取到连接:

注意:上述代码中的 DruidDataSourceFactory要选 com.alibaba.druid.pool 下面的,如下图:


5.3,HikariCP连接池的使用

如果采用硬编码方式实现(可只做了解,不推荐使用),Java代码示例如下:

java 复制代码
@Test
public void testHardCodeHikari() throws SQLException {
    /*
     硬编码:将连接池的配置信息和Java代码耦合在一起。
     1、创建HikariDataSource连接池对象
     2、设置连接池的配置信息
     3、通过连接池获取连接对象
     4、回收连接
     */
    //1.创建HikariDataSource连接池对象
    HikariDataSource hikariDataSource = new HikariDataSource();

    //2.设置连接池的配置信息【必须 | 非必须】
    //2.1必须设置的配置
    hikariDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    hikariDataSource.setJdbcUrl("jdbc:mysql://localhost:3306/JDBC");
    hikariDataSource.setUsername("your_username");
    hikariDataSource.setPassword("your_password");

    //2.2 非必须设置的配置
    hikariDataSource.setMinimumIdle(10);
    hikariDataSource.setMaximumPoolSize(20);

    //3.通过连接池获取连接对象
    Connection connection = hikariDataSource.getConnection();

    System.out.println(connection);

    //回收连接
    connection.close();
}

为了方便代码的更新和维护,更推荐使用如下的软编码方式,将配置文件和Java代码解耦。

① 首先在 resources 目录下创建 hikari.properties 配置文件,并写入hikari 连接池配置信息

bash 复制代码
driverClassName=com.mysql.cj.jdbc.Driver
jdbcUrl=jdbc:mysql://localhost:3306/JDBC
username=your_username
password=your_password
minimumIdle=10
maximumPoolSize=20

② 软编码Java代码如下:

java 复制代码
@Test
public void testResourcesHikari()throws Exception{
    //1.创建Properties集合,用于存储外部配置文件的key和value值。
    Properties properties = new Properties();

    //2.读取外部配置文件,获取输入流,加载到Properties集合里。
    InputStream inputStream = HikariTest.class.getClassLoader().getResourceAsStream("hikari.properties");
    properties.load(inputStream);

    // 3.创建Hikari连接池配置对象,将Properties集合传进去(和Durid存在区别)
    HikariConfig hikariConfig = new HikariConfig(properties);

    // 4. 基于Hikari配置对象,构建连接池
    HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig);

    // 5. 获取连接
    Connection connection = hikariDataSource.getConnection();
    System.out.println("connection = " + connection);

    //6.回收连接
    connection.close();
}
相关推荐
applebomb5 小时前
经济、能打的vibe coding后端组合:IDEA+Claude Code(WSL)+GLM4.6安装与配置
java·idea·ai编程·vibecoding
布伦鸽5 小时前
C# WPF -MaterialDesignTheme 找不到资源“xxx“问题记录
开发语言·c#·wpf
yuezhilangniao5 小时前
PostgreSQL vs MySQL:从零开始基础命令对比指南
数据库·mysql·postgresql
枫叶丹45 小时前
【Qt开发】Qt窗口(十) -> QInputDialog 输入对话框
c语言·开发语言·数据库·c++·qt
杰瑞不懂代码5 小时前
基于 MATLAB 的 BPSK 在 AWGN 信道下误码率仿真与性能分析
开发语言·网络·matlab
zore_c5 小时前
【C语言】EasyX图形库——实现游戏音效(详解)(要游戏音效的看过来!!!)
c语言·开发语言·经验分享·笔记·游戏
qq_381454995 小时前
设计模式详解:代码架构的艺术
开发语言·javascript·ecmascript
行走的陀螺仪5 小时前
Vue3 项目单元测试全指南:价值、Vitest 落地与提效方案
开发语言·前端·单元测试·html5·vitest
程序媛青青5 小时前
MVCC 原理
服务器·数据库·oracle