精选专栏链接 🔗
欢迎订阅,点赞+关注,每日精进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();
}