万字JDBC教程
[学习地址](【尚硅谷2024最新JDBC教程 | jdbc基础到高级一套通关!】 www.bilibili.com/video/BV1Tx…)
建立项目如图:
并执行sql语句:
CREATE DATABASE rainsoul;
use rainsoul;
create table t_emp
(
emp_id int auto_increment comment '员工编号' primary key,
emp_name varchar(100) not null comment '员工姓名',
emp_salary double(10, 5) not null comment '员工薪资',
emp_age int not null comment '员工年龄'
);
insert into t_emp (emp_name, emp_salary, emp_age)
values ('andy', 777.77, 32),
('大风哥', 666.66, 41),
('康师傅', 111, 23),
('Gavin', 123, 26),
('小鱼儿', 123, 28);
2.快速入门
查询数据库所有信息:
package com.rainsoul.base;
import java.sql.*;
public class JDBCQuick {
/**
* 主函数:演示从MySQL数据库中查询员工信息并打印。
* @param args 命令行参数(未使用)
* @throws ClassNotFoundException 如果MySQL JDBC驱动未找到,则抛出此异常
* @throws SQLException 如果数据库操作出现错误,则抛出此异常
*/
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 注册MySQL JDBC驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取到数据库的连接
Connection connection = DriverManager
.getConnection("jdbc:mysql://localhost:3306/rainsoul", "root", "root");
// 创建PreparedStatement对象,用于执行预编译的SQL查询
PreparedStatement preparedStatement = connection
.prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp");
// 执行查询语句,获取结果集
ResultSet resultSet = preparedStatement.executeQuery();
// 处理查询结果,将每条员工信息打印出来
while (resultSet.next()) {
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
String empSalary = resultSet.getString("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId + "\t" + empName + "\t" + empSalary + "\t" + empAge);
}
// 释放资源,关闭ResultSet、PreparedStatement以及Connection
resultSet.close();
preparedStatement.close();
connection.close();
}
}
查询结果:
3.核心API流程的理解
3.1注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
-
在 Java 中,当使用 JDBC(Java Database Connectivity)连接数据库时,需要加载数据库特定的驱动程序,以便与数据库进行通信。加载驱动程序的目的是为了注册驱动程序,使得 JDBC API 能够识别并与特定的数据库进行交互。
-
从JDK6开始,不再需要显式地调用
Class.forName()
来加载 JDBC 驱动程序,只要在类路径中集成了对应的jar文件,会自动在初始化时注册驱动程序。
在Java程序中执行 Class.forName("com.mysql.cj.jdbc.Driver") 这一行代码时,以下底层过程会依次发生: 类加载:
- JVM接收到这个字符串字面量,根据它来查找并加载指定名称的类。类加载是通过类加载器(ClassLoader)体系实现的。通常情况下,系统类加载器(System ClassLoader)负责加载用户类路径(classpath)上的类。类加载包括三个阶段:加载(Loading)、验证(Verification)、准备(Preparation)。在这个过程中,JVM会查找指定类的.class文件(对于MySQL JDBC驱动来说,通常位于jar包中),将其字节码数据读入内存,并进行必要的安全性和正确性检查。
- 类初始化:在类加载完成后,如果该类尚未被初始化,JVM会触发类的初始化过程。类初始化包括分配静态变量内存、执行静态初始化块(static {})中的代码等步骤。对于 com.mysql.cj.jdbc.Driver 类,其内部通常包含一个静态初始化块或静态方法,在类初次被加载时会被自动执行。这个初始化逻辑的核心任务是向Java的JDBC驱动管理器(java.sql.DriverManager)注册该驱动。
- 驱动注册:com.mysql.cj.jdbc.Driver 类(或者其他MySQL JDBC驱动类,如旧版的 com.mysql.jdbc.Driver)会实现 java.sql.Driver 接口。按照JDBC规范,实现该接口的类需要在初始化时向 DriverManager 注册自己。注册通常是通过调用 DriverManager.registerDriver() 方法完成的,但在MySQL JDBC驱动中,可能采用更现代的Service Provider Interface (SPI)机制,即在类路径下的 META-INF/services/java.sql.Driver 文件中声明该驱动类。这样,当JDBC API尝试加载驱动时,会自动发现并加载这个文件中列出的实现类。注册过程实质上是将驱动类的一个实例(通常是一个单例或无状态对象)添加到 DriverManager 内部的驱动列表中。这样,当后续调用 DriverManager.getConnection() 方法时,DriverManager 就知道有哪些可用的驱动可以用来尝试建立数据库连接。
我用个生活中的比喻解释一下:
想象你奶奶想打个电话,但是家里没有电话机。为了能打电话,首先需要去买一个电话机,把它正确安装好并插到电话线上。这相当于加载MySQL的驱动程序类。
家里买来的电话机不会自动连到电话线上,必须手动把它安装插好才能用。同样地,刚加载的MySQL驱动也不会自动可用,必须手动将它"注册"到管理所有电话机的系统中。
这个"注册"的过程,就好比你需要拨一个特殊的号码,把新买的电话机报给电话公司,告诉他们:"嘿,我家新装了一个电话机,请把它加入你们的系统里"。
一旦电话公司的系统里有了你家的新电话机,将来就可以用它打电话了。同理,注册后的MySQL驱动也可以被Java程序使用,连接数据库了。
所以那行Class.forName
代码,相当于你先买回家一个电话机(加载驱动);而在它的静态代码块中,则自动帮你打了个内线电话给电话公司(注册驱动),"嘿,我是新的MySQL电话机,请记录我"。
3.2Connection
-
Connection接口是JDBC API的重要接口,用于建立与数据库的通信通道。换而言之,Connection对象不为空,则代表一次数据库连接。
-
在建立连接时,需要指定数据库URL、用户名、密码参数。
- URL:jdbc:mysql://localhost:3306/atguigu
- jdbc:mysql://IP地址:端口号/数据库名称?参数键值对1&参数键值对2
-
Connection
接口还负责管理事务,Connection
接口提供了commit
和rollback
方法,用于提交事务和回滚事务。 -
可以创建
Statement
对象,用于执行 SQL 语句并与数据库进行交互。 -
在使用JDBC技术时,必须要先获取Connection对象,在使用完毕后,要释放资源,避免资源占用浪费及泄漏。
3.3Statement
-
Statement
接口用于执行 SQL 语句并与数据库进行交互。它是 JDBC API 中的一个重要接口。通过Statement
对象,可以向数据库发送 SQL 语句并获取执行结果。 -
结果可以是一个或多个结果。
- 增删改:受影响行数单个结果。
- 查询:单行单列、多行多列、单行多列等结果。
-
但是
Statement
接口在执行SQL语句时,会产生SQL注入攻击问题
:- 当使用
Statement
执行动态构建的 SQL 查询时,往往需要将查询条件与 SQL 语句拼接在一起,直接将参数和SQL语句一并生成,让SQL的查询条件始终为true得到结果。
- 当使用
示例代码:
String username = "' OR '1'='1"; // 用户输入的非法用户名
String sql = "SELECT * FROM users WHERE username='" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 结果集将包含整个 users 表的所有记录
在这段代码中,我们动态构建了一个 SQL 查询语句,并将用户输入的username
直接拼接到 SQL 语句中。但是,用户输入的用户名是一个非法的字符串 ' OR '1'='1
。
当执行 SQL 语句时,实际执行的是:
SELECT * FROM users WHERE username='' OR '1'='1'
由于'1'='1'
这个条件永远为真,因此该查询会返回 users 表中的所有记录,而不是预期的基于用户名进行过滤的结果。
这种注入攻击发生的根本原因是,用户的输入数据被直接拼接到 SQL 语句中,而没有经过任何过滤和检查。攻击者可以构造特殊的字符串,影响原本的 SQL 语句执行逻辑,从而绕过认证或获取不应有的数据。
这种 SQL 注入攻击不仅可能导致数据泄露,还可能被利用执行任意的 SQL 语句,如删除或修改数据库数据等,造成严重的安全隐患。
因此,在使用 JDBC 时,应当避免使用Statement
执行动态构建的 SQL 语句,而是使用PreparedStatement
。PreparedStatement
允许使用参数占位符,将参数数据与 SQL 语句分离,有效防止 SQL 注入攻击的发生。
dubug演示:
代码:
package com.rainsoul.base;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Scanner;
public class JDBCInjection {
public static void main(String[] args) throws Exception {
//1.注册驱动 (可以省略)
//2.获取连接对象
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
//3.获取执行SQL语句对象
Statement statement = connection.createStatement();
System.out.println("请输入员工姓名:");
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();
//4.编写SQL语句,并执行,接受返回的结果
String sql = "SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_name = '"+name+"'";
ResultSet resultSet = statement.executeQuery(sql);
//5.处理结果:遍历resultSet
while(resultSet.next()){
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
//6.释放资源
resultSet.close();
statement.close();
connection.close();
}
}
3.4 PreparedStatement
PreparedStatement
是Statement
接口的子接口,用于执行预编译
的 SQL 查询,作用如下:- 预编译SQL语句:在创建PreparedStatement时,就会预编译SQL语句,也就是SQL语句已经固定。
- 防止SQL注入:
PreparedStatement
支持参数化查询,将数据作为参数传递到SQL语句中,采用?占位符的方式,将传入的参数用一对单引号包裹起来'',无论传递什么都作为值。有效防止传入关键字或值导致SQL注入问题。 - 性能提升:PreparedStatement是预编译SQL语句,同一SQL语句多次执行的情况下,可以复用,不必每次重新编译和解析。
代码:
package com.rainsoul.base;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Scanner;
public class JDBCPrepared {
public static void main(String[] args) throws Exception{
//1.注册驱动 (可以省略)
//2.获取连接对象
Connection connection = DriverManager
.getConnection("jdbc:mysql:///rainsoul", "root", "root");
//3.获取执行SQL语句对象
PreparedStatement preparedStatement = connection
.prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_name = ?");
System.out.println("请输入员工姓名:");
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();
//4.为?占位符复制,并执行SQL语句,接受返回的结果
preparedStatement.setString(1, name);
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果:遍历resultSet
while(resultSet.next()){
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
//6.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
3.5ResultSet
ResultSet
是 JDBC API 中的一个接口,用于表示从数据库中执行查询语句所返回的结果集
。它提供了一种用于遍历和访问查询结果的方式。- 遍历结果:ResultSet可以使用
next()
方法将游标移动到结果集的下一行,逐行遍历数据库查询的结果,返回值为boolean类型,true代表有下一行结果,false则代表没有。 - 获取单列结果:可以通过getXxx的方法获取单列的数据,该方法为重载方法,支持索引和列名进行获取。
4. 基于PreparedStatement实现CRUD
4.1查询单行单列
@Test
public void testQuerySingleRowAndCol() throws SQLException {
//1.注册驱动 (可以省略)
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
//3.预编译SQL语句得到PreparedStatement对象
PreparedStatement preparedStatement = connection.prepareStatement("SELECT COUNT(*) as count FROM t_emp");
//4.执行SQL语句,获取结果
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果(如果自己明确一定只有一个结果,那么resultSet最少要做一次next的判断,才能拿到我们要的列的结果)
if(resultSet.next()){
int count = resultSet.getInt("count");
System.out.println(count);
}
//6.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
4.2查询单行多列
@Test
public void testQuerySingleRow()throws Exception{
//1.注册驱动
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
//3.预编译SQL语句获得PreparedStatement对象
PreparedStatement preparedStatement = connection.prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_id = ?");
//4.为占位符赋值,然后执行,并接受结果
preparedStatement.setInt(1,5);
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果
while(resultSet.next()){
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
//6.资源释放
resultSet.close();
preparedStatement.close();
connection.close();
}
4.3查询多行多列
@Test
public void testQueryMoreRow()throws Exception{
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_age > ?");
//为占位符赋值,执行SQL语句,接受结果
preparedStatement.setInt(1, 25);
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()){
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
resultSet.close();
preparedStatement.close();
connection.close();
}
4.4新增
@Test
public void testInsert() throws SQLException {
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)");
preparedStatement.setString(1, "rose");
preparedStatement.setDouble(2,345.67);
preparedStatement.setInt(3,28);
int result = preparedStatement.executeUpdate();
//根据受影响行数,做判断,得到成功或失败
if(result > 0){
System.out.println("成功!");
}else{
System.out.println("失败!");
}
preparedStatement.close();
connection.close();
}
4.5修改
@Test
public void testUpdate() throws SQLException {
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE t_emp SET emp_salary = ? WHERE emp_id = ?");
preparedStatement.setDouble(1, 888.88);
preparedStatement.setInt(2, 6);
int result = preparedStatement.executeUpdate();
if(result > 0){
System.out.println("成功!");
}else{
System.out.println("失败!");
}
preparedStatement.close();
connection.close();
}
4.6删除
@Test
public void testDelete() throws SQLException {
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/rainsoul", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM t_emp WHERE emp_id = ?");
preparedStatement.setDouble(1, 6);
int result = preparedStatement.executeUpdate();
if(result > 0){
System.out.println("成功!");
}else{
System.out.println("失败!");
}
preparedStatement.close();
connection.close();
}
5.JDBC扩展
5.1实体类和ORM
- 在使用JDBC操作数据库时,我们会发现数据都是零散的,明明在数据库中是一行完整的数据,到了Java中变成了一个一个的变量,不利于维护和管理。而我们Java是面向对象的,一个表对应的是一个类,一行数据就对应的是Java中的一个对象,一个列对应的是对象的属性,所以我们要把数据存储在一个载体里,这个载体就是实体类!
- ORM(Object Relational Mapping)思想,对象到关系数据库的映射,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,以面向对象的角度操作数据库中的数据,即一张表对应一个类,一行数据对应一个对象,一个列对应一个属性!
- 当下JDBC中这种过程我们称其为手动ORM。后续我们也会学习ORM框架,比如MyBatis、JPA等。
新建POJO类:
//类名和数据库名对应,但是表名一般缩写,类名要全写!
public class Employee {
private Integer empId;//emp_id = empId 数据库中列名用下划线分隔,属性名用驼峰!
private String empName;//emp_name = empName
private Double empSalary;//emp_salary = empSalary
private Integer empAge;//emp_age = empAge
get set...
}
封装代码:
@Test
public void testORM() throws SQLException {
// 获取数据库连接
Connection connection = DriverManager
.getConnection("jdbc:mysql:///rainsoul", "root", "root");
// 准备执行SQL语句的预编译语句,用于提高查询效率和防止SQL注入
PreparedStatement preparedStatement = connection
.prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp where emp_id = ?");
// 设置预编译语句中的参数
preparedStatement.setInt(1,1);
// 执行查询并获取结果集
ResultSet resultSet = preparedStatement.executeQuery();
// 初始化Employee对象,用于存放查询结果
Employee employee = null;
// 遍历结果集并将数据赋值给Employee对象
if(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对象的属性上
employee.setEmpId(empId);
employee.setEmpName(empName);
employee.setEmpSalary(empSalary);
employee.setEmpAge(empAge);
}
// 打印Employee对象,验证查询和映射结果
System.out.println(employee);
// 关闭结果集、预编译语句和数据库连接,释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
5.2主键回显
在数据中,执行新增操作时,主键列为自动增长,可以在表中直观的看到,但是在Java程序中,我们执行完新增后,只能得到受影响行数,无法得知当前新增数据的主键值。在Java程序中获取数据库中插入新数据后的主键值,并赋值给Java对象,此操作为主键回显。
@Test
public void testReturnPK()throws Exception{
//获取连接
Connection connection = DriverManager
.getConnection("jdbc:mysql:///rainsoul", "root", "root");
// 预编译SQL语句,告知preparedStatement,返回新增数据的主键列的值
String sql = "INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
// 创建对象,将对象的属性值,填充在?占位符上 (ORM)
Employee employee = new Employee(null, "jack", 123.45, 29);
preparedStatement.setString(1, employee.getEmpName());
preparedStatement.setDouble(2, employee.getEmpSalary());
preparedStatement.setInt(3, employee.getEmpAge());
//执行SQL,并获取返回的结果
int result = preparedStatement.executeUpdate();
ResultSet resultSet = null;
//处理结果
if(result > 0){
System.out.println("成功!");
//获取当前新增数据的主键列,回显到Java中employee对象的empId属性上。
//返回的主键值,是一个单行单列的结果存储在ResultSet里
resultSet = preparedStatement.getGeneratedKeys();
if(resultSet.next()){
int empId = resultSet.getInt(1);
employee.setEmpId(empId);
}
System.out.println(employee);
}else{
System.out.println("失败!");
}
//释放资源
if(resultSet!=null){
resultSet.close();
}
preparedStatement.close();
connection.close();
}
5.3批量操作
一条一条操作很耗时间:
@Test
public void testReturnPK()throws Exception{
//获取连接
Connection connection = DriverManager
.getConnection("jdbc:mysql:///rainsoul", "root", "root");
// 预编译SQL语句,告知preparedStatement,返回新增数据的主键列的值
String sql = "INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
// 创建对象,将对象的属性值,填充在?占位符上 (ORM)
Employee employee = new Employee(null, "jack", 123.45, 29);
preparedStatement.setString(1, employee.getEmpName());
preparedStatement.setDouble(2, employee.getEmpSalary());
preparedStatement.setInt(3, employee.getEmpAge());
//执行SQL,并获取返回的结果
int result = preparedStatement.executeUpdate();
ResultSet resultSet = null;
//处理结果
if(result > 0){
System.out.println("成功!");
//获取当前新增数据的主键列,回显到Java中employee对象的empId属性上。
//返回的主键值,是一个单行单列的结果存储在ResultSet里
resultSet = preparedStatement.getGeneratedKeys();
if(resultSet.next()){
int empId = resultSet.getInt(1);
employee.setEmpId(empId);
}
System.out.println(employee);
}else{
System.out.println("失败!");
}
//释放资源
if(resultSet!=null){
resultSet.close();
}
preparedStatement.close();
connection.close();
}
优化后:
@Test
public void testBatch() throws Exception {
//1.注册驱动
// Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul?rewriteBatchedStatements=true", "root", "root");
//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();
}
6.连接池
我们每次操作数据库都要获取新连接,使用完毕后就close释放,频繁的创建和销毁造成资源浪费。连接的数量无法把控,对服务器来说压力巨大。
连接池就是数据库连接对象的缓冲区,通过配置,由连接池负责创建连接、管理连接、释放连接等操作。
预先创建数据库连接放入连接池,用户在请求时,通过池直接获取连接,使用完毕后,将连接放回池中,避免了频繁的创建和销毁,同时解决了创建的效率。
当池中无连接可用,且未达到上限时,连接池会新建连接。
池中连接达到上限,用户请求会等待,可以设置超时时间。
好的,下面是常见的几种连接池及其简要讲解:
连接池 | 描述 |
---|---|
Apache DBCP | Apache 开发的数据库连接池,实现了连接池的基本管理功能,并对获取连接、使用连接、释放连接等过程进行了简单的封装,是一个老牌、经典的连接池。 |
C3P0 | 一个开源的JDBC连接池产品,实现了数据源和连接池的功能。它支持JDBC3规范和JDBC2的扩展,包括连接分组、强制断线重连、统计和扩展语句级别等功能。 |
Tomcat JDBC Connection Pool | Tomcat服务器自带的数据库连接池,可以直接使用或集成到其他服务器环境中。相比Apache DBCP和C3P0,它配置更简单,且内置于Tomcat中,占用资源较少。 |
HikariCP | 一个轻量级的高性能连接池,它是为了解决Apache DBCP、C3P0等连接池存在的并发性能问题和内存泄漏问题而设计的。相比其他连接池,HikariCP更快、更稳定,资源利用率更高。 |
Druid | Alibaba开源的一个数据库连接池项目,它包含一个高效的连接池和监控组件。Druid能够提供强大的监控和扩展功能,帮助开发人员快速发现系统中存在的问题。 |
BoneCP | 一个极简连接池实现,它在性能和低内存占用方面表现良好。BoneCP设计为轻量级并且容易使用,但它缺乏其他连接池提供的一些高级功能。 |
选择合适的连接池要考虑项目的实际需求、性能要求、可维护性等因素。一般来说,HikariCP和Druid因为性能和功能方面的出色表现,使用较为广泛。Apache DBCP和C3P0作为传统的连接池也有一定使用。不同场景下,可以根据具体情况选择适合的连接池产品。
6.1Druid连接池使用
首先要引入jar包。
/**
* 测试通过硬编码方式使用Druid连接池。
* 该方法展示了如何直接在代码中配置Druid连接池,并使用该连接池获取数据库连接,
* 进行操作后,再将连接归还给连接池。
*
* @throws SQLException 如果操作数据库连接时发生错误,则抛出SQLException。
*/
@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:///rainsoul");
druidDataSource.setUsername("root");
druidDataSource.setPassword("root");
//2.2 非必须设置的配置,例如初始化大小和最大活动连接数
druidDataSource.setInitialSize(10);
druidDataSource.setMaxActive(20);
//3.通过连接池获取连接对象
Connection connection = druidDataSource.getConnection();
System.out.println(connection);
//基于connection进行数据库操作,例如CRUD
//4.回收连接,将使用完毕的连接归还给连接池
connection.close();
}
使用配置文件:
/**
* 测试通过Druid连接池获取数据库连接的功能
* 本测试方法不接受参数,也不返回任何值
* @throws Exception 抛出异常的条件:读取配置文件或获取数据库连接时可能发生的任何异常
*/
@Test
public void testResourcesDruid() throws Exception {
// 创建Properties对象用于存放配置信息
Properties properties = new Properties();
// 从类路径下读取db.properties配置文件,并加载到Properties对象中
InputStream inputStream = DruidTest.class.getClassLoader()
.getResourceAsStream("db.properties");
properties.load(inputStream);
// 使用DruidDataSourceFactory和配置信息创建DruidDataSource连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 从连接池中获取一个数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
// 此处是开发CRUD操作的代码位置,当前代码未实现具体操作
// 使用完毕后关闭数据库连接,释放资源
connection.close();
}
6.2Hikari连接池的使用
@Test
public void testHardCodeHikari()throws Exception{
/*
硬编码:将连接池的配置信息和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:///rainsoul");
hikariDataSource.setUsername("root");
hikariDataSource.setPassword("root");
//2.2 非必须设置的配置
hikariDataSource.setMinimumIdle(10);
hikariDataSource.setMaximumPoolSize(20);
//3.通过连接池获取连接对象
Connection connection = hikariDataSource.getConnection();
System.out.println(connection);
//4.回收连接
connection.close();
}
使用配置文件:
@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.创建HikariConfig连接池配置对象,将Properties集合传进去。
HikariConfig hikariConfig = new HikariConfig(properties);
//4.基于HikariConfig连接池配置对象,构建HikariDataSource
HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig);
//5.获取连接
Connection connection = hikariDataSource.getConnection();
System.out.println(connection);
//6.回收连接
connection.close();
}
6.3常见的参数配置
参数名称 | 描述 |
---|---|
initialSize | 连接池初始连接数量,默认值通常为0 |
maxActive | 连接池在同一时间能够分配的最大活跃连接数,使用负值表示不限制 |
maxIdle | 连接池中最大空闲连接数,控制池中有多少空闲连接可以存活 |
minIdle | 连接池中最小空闲连接数,低于这个数量时,连接池会创建新的连接 |
maxWait | 当没有可用连接时,连接池等待连接被归还的最大时间(以毫秒计),超过时间则抛出异常 |
maxAge | 连接池中连接能够存活的最长时间(以毫秒计),超过时间将被释放 |
testOnBorrow | 在将连接借出时是否测试连接的有效性,可避免将无效连接分配出去 |
testOnReturn | 在将连接归还到池中时是否测试连接的有效性,可避免将无效连接存入池中 |
testWhileIdle | 是否对空闲连接进行有效性检测,可避免连接由于长期空闲而失效 |
timeBetweenEvictionRunsMillis | 空闲连接检测线程的运行周期时间,用于控制空闲连接检测的频率 |
validationQuery | 用于检测连接是否有效的SQL查询语句,如 "SELECT 1" |
removeAbandonedTimeout | 连接长时间无操作时,被视为已经被废弃的超时时间(以秒计) |
logAbandoned | 是否记录长时间无操作而被废弃的连接信息 |
根据不同的连接池实现,可配置参数的名称可能有所不同,但基本含义是相似的。正确配置这些参数可以合理控制连接池的大小、连接的生命周期、资源利用率等,从而优化连接池的性能和资源占用。
7.JDBC优化及工具类封装
7.1JDBC工具类(V1.0):
- 维护一个连接池对象。
- 对外提供在连接池中获取连接的方法
- 对外提供回收连接的方法 注意:工具类仅对外提供共性的功能代码,所以方法均为静态方法!
这段代码实现了一个JDBC工具类 JDBCUtil
,用于管理数据库连接池和获取/释放连接。我将为每部分代码添加注释进行解释。
public class JDBCUtil {
// 创建连接池引用,因为要提供给当前项目的全局使用,所以创建为静态的。
private static DataSource dataSource;
// 在项目启动时,即创建连接池对象,赋值给dataSource
static {
try {
// 创建一个Properties对象,用于加载配置文件
Properties properties = new Properties();
// 获取配置文件的输入流
InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
// 加载配置文件
properties.load(inputStream);
// 使用Druid连接池工厂类创建连接池对象,并传入配置文件参数
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
// 如果创建连接池出现异常,则抛出运行时异常
throw new RuntimeException(e);
}
}
// 对外提供在连接池中获取连接的方法
public static Connection getConnection() {
try {
// 从连接池中获取一个连接
return dataSource.getConnection();
} catch (SQLException e) {
// 如果获取连接出现异常,则抛出运行时异常
throw new RuntimeException(e);
}
}
// 对外提供回收连接的方法
public static void release(Connection connection) {
try {
// 关闭连接,将其归还到连接池中
connection.close();
} catch (SQLException e) {
// 如果关闭连接出现异常,则抛出运行时异常
throw new RuntimeException(e);
}
}
}
这个工具类主要实现了以下功能:
- 在静态代码块中,通过读取配置文件
db.properties
获取数据库连接参数,并使用 Druid 连接池工厂类创建一个连接池对象dataSource
。 - 提供一个静态方法
getConnection()
,用于从连接池中获取一个数据库连接。 - 提供一个静态方法
release(Connection connection)
,用于关闭指定的数据库连接,将其归还到连接池中。
使用这个工具类,可以方便地获取和释放数据库连接,同时利用连接池技术提高连接的复用率和性能。需要注意的是,在项目启动时就创建了连接池对象,并在静态代码块中加载了配置文件,这种方式确保了连接池的初始化只执行一次。
7.2ThreadLocal
用一个生活中的例子来解释什么是ThreadLocal
以及它的使用场景。
我们可以把ThreadLocal
想象成一个小柜子,每个人都有自己专属的小柜子。这个小柜子就相当于每个线程内部的存储空间。
假设你奶奶有几个孙子孙女,他们都很顽皮,经常在家里到处乱跑。为了防止他们弄乱房间,你给每个孩子准备了一个专属的小柜子,让他们把自己的东西放进去。
这样一来,每个孩子拿自己柜子里的东西就不会影响其他孩子,也不会把房间弄乱了。这就是ThreadLocal
的作用,它为每个"线程"提供了一个独立的存储空间,可以在里面放自己的东西,而不会影响到其他"线程"。
在程序中,多个线程就像你奶奶家的这些孩子一样,他们并发地运行,有可能会争夺一些共享的资源(比如数据库连接对象)。如果多个线程使用同一个资源,就可能会导致线程安全问题。
通过ThreadLocal
,每个线程就拥有了自己独立的"小柜子",可以放置自己的连接对象。当线程需要使用连接时,从自己的"小柜子"里取出来用;用完后,又放回到"小柜子"中,不会影响其他线程。
这种做法避免了线程之间相互影响和频繁地从连接池中获取连接,提高了程序的运行效率和线程安全性。
当然,我们要记得在线程结束后,将"小柜子"里的东西清理干净,避免浪费资源。否则就像孩子长大后忘记把自己的旧东西清理出柜子一样,会导致资源的浪费和积累。
总之,ThreadLocal
就像是为每个线程准备了一个"独立的小空间",保证了线程之间使用共享资源时的隔离性和安全性。只要用好了,就可以让程序运行得更高效和可靠。
ThreadLocal的相关知识点整理如下:
-
ThreadLocal概念
- ThreadLocal是JDK包中的一个类,可以在同一个线程内创建独立的副本变量。
- 每个线程都会有一个自己独立的副本变量,线程之间彼此不会相互影响。
- ThreadLocal提供get()、set(value)、remove()等方法访问和修改副本变量。
-
ThreadLocal原理
- 每个Thread内部都有一个ThreadLocalMap类型的成员变量,存储当前线程的副本变量。
- ThreadLocal作为Map的键(key),副本变量作为Map的值(value)存储。
- 每个线程读写自己所属线程的变量副本,线程之间相互隔离。
-
ThreadLocal使用场景
- 线程本地存储(每个线程内部有自己独立的变量副本)
- 事务管理(绑定事务上下文)
- 日志记录(MDC机制存储日志跟踪信息)
- 数据库连接池(每个线程有独立的连接对象)
-
ThreadLocal使用注意事项
- 每个线程自己使用后,需要调用remove()清除副本,避免内存泄漏。
- 副本变量是存储在线程对象内部的Map中,生存周期随线程终止而销毁。
- 减小副本变量的范围,使用后再强制设为null,避免副本变量带来的额外内存占用。
- 只有在状态确实需要与线程相关联时,才使用ThreadLocal存储状态。
-
ThreadLocal应用实例
- JDBC中使用ThreadLocal获取线程独立的Connection对象
- Servlet组件绑定和传递Request、Response等对象
- 实现简化的线程上下文管理器(TransmittableThreadLocal)
-
ThreadLocal内存泄漏风险
- 由于ThreadLocalMap的生存周期是与线程一直存在的,若不手动remove,就会导致内存泄漏。
- 使用弱引用或定期删除无效线程变量,防止内存泄漏。
- 在线程池中注意每次运行结束后,清理ThreadLocal存储。
JDBCUtilV2工具类(V2.0)
这个 JDBCUtilV2
类是在原有 JDBCUtil
类的基础上进行了改进,增加了 ThreadLocal
的支持。我将为这部分新增的代码添加注释进行解释。
public class JDBCUtilV2 {
// ...
// 创建一个ThreadLocal对象,用于存储线程级别的Connection对象
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
// ...
// 对外提供在连接池中获取连接的方法
public static Connection getConnection() {
try {
// 从ThreadLocal中获取当前线程绑定的Connection对象
Connection connection = threadLocal.get();
// 如果ThreadLocal中没有绑定Connection对象
if (connection == null) {
// 从连接池中获取一个新的连接
connection = dataSource.getConnection();
// 将新的连接对象绑定到当前线程的ThreadLocal中
threadLocal.set(connection);
}
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
// 对外提供回收连接的方法
public static void release() {
try {
// 从ThreadLocal中获取当前线程绑定的Connection对象
Connection connection = threadLocal.get();
if (connection != null) {
// 从当前线程的ThreadLocal中移除绑定的Connection对象
threadLocal.remove();
// 如果开启了事务的手动提交,操作完毕后,归还给连接池之前,要将事务的自动提交改为true
connection.setAutoCommit(true);
// 将连接对象归还给连接池
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
这个改进的版本利用了 ThreadLocal
这个线程内部的数据存储类,实现了每个线程拥有自己独立的 Connection
对象。具体来说:
- 声明了一个静态的
ThreadLocal<Connection>
对象threadLocal
。 - 在
getConnection()
方法中,首先尝试从当前线程的ThreadLocal
中获取Connection
对象。如果获取不到,则从连接池中获取一个新的连接,并将其绑定到当前线程的ThreadLocal
中。 - 在
release()
方法中,从当前线程的ThreadLocal
中获取绑定的Connection
对象。如果存在,则先将其从ThreadLocal
中移除,然后将连接的自动提交设置为true
(如果之前手动设置过),最后将连接归还给连接池。
使用 ThreadLocal
的好处是每个线程都可以独立获取和使用自己的 Connection
对象,避免了多线程环境下的线程安全问题。同时,在操作完成后,通过调用 release()
方法,可以将 Connection
对象正确地归还给连接池,实现了连接的复用。
需要注意的是,在使用 ThreadLocal
时,必须确保在线程结束后正确地释放资源,否则可能会导致内存泄漏。在这个例子中,由于 ThreadLocal
对象是静态的,所以需要通过其他方式(如在Web应用中监听ServletContextListener
事件)来清理 ThreadLocal
。
拿到同一个。
8.DAO封装及BaseDAO工具类
DAO模式的几个核心点:
-
职责划分:DAO层专注于对数据库的访问操作,不涉及业务逻辑,将数据库操作与业务逻辑解耦。业务逻辑由Service层负责处理。
-
面向对象:Java天生面向对象,一个DAO对象对应一张数据库表,每个DAO对象维护这张表的CRUD(增删改查)操作方法。
-
封装性:DAO对象对外提供标准化的数据访问API接口,上层模块无需关心数据库访问的具体实现细节。
-
易于维护:数据库操作均集中在DAO层,如果需要更换底层数据存储方式,只需修改DAO层即可,上层代码无需改动。
-
复用性:通用的数据访问方法可以在DAO层得到复用,提高代码的重用率。
-
规范性:引入DAO层有利于遵循设计模式规范,促进项目结构清晰和可维护性。
基本上每一个数据表都应该有一个对应的DAO接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些DAO的实现类可以抽取一个公共的父类,复用增删改查的基本操作,我们称为BaseDAO。
8.1创建员工DAO接口
/**
* EmployeeDao这个类对应的是t_emp这张表的增删改查的操作
*/
public interface EmployeeDao {
/**
* 数据库对应的查询所有的操作
* @return 表中所有的数据
*/
List<Employee> selectAll();
/**
* 数据库对应的根据empId查询单个员工数据操作
* @param empId 主键列
* @return 一个员工对象(一行数据)
*/
Employee selectByEmpId(Integer empId);
/**
* 数据库对应的新增一条员工数据
* @param employee ORM思想中的一个员工对象
* @return 受影响的行数
*/
int insert(Employee employee);
/**
* 数据库对应的修改一条员工数据
* @param employee ORM思想中的一个员工对象
* @return 受影响的行数
*/
int update(Employee employee);
/**
* 数据库对应的根据empId删除一条员工数据
* @param empId 主键列
* @return 受影响的行数
*/
int delete(Integer empId);
}
public class EmployeeDaoImpl extends BaseDAO implements EmployeeDao {
@Override
public List<Employee> selectAll() {
try {
String sql = "SELECT emp_id empId,emp_name empName,emp_salary empSalary,emp_age empAge FROM t_emp";
return executeQuery(Employee.class,sql,null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Employee selectByEmpId(Integer empId) {
try {
String sql = "SELECT emp_id empId,emp_name empName,emp_salary empSalary,emp_age empAge FROM t_emp where emp_id = ?";
return executeQueryBean(Employee.class,sql,empId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int insert(Employee employee) {
try {
String sql = "INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)";
return executeUpdate(sql,employee.getEmpName(),employee.getEmpSalary(),employee.getEmpAge());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int update(Employee employee) {
try {
String sql = "UPDATE t_emp SET emp_salary = ? WHERE emp_id = ?";
return executeUpdate(sql,employee.getEmpSalary(),employee.getEmpId());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int delete(Integer empId) {
try {
String sql = "delete from t_emp where emp_id = ?";
return executeUpdate(sql,empId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
8.2BaseDAO搭建
/**
* 将共性的数据库的操作代码封装在BaseDAO里。
*/
public class BaseDAO {
/**
* 通用的增删改的方法。
* @param sql 调用者要执行的SQL语句
* @param params SQL语句中的占位符要赋值的参数
* @return 受影响的行数
*/
public int executeUpdate(String sql,Object... params)throws Exception{
//1.通过JDBCUtilV2获取数据库连接
Connection connection = JDBCUtilV2.getConnection();
//2.预编译SQL语句
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//4.为占位符赋值,执行SQL,接受返回结果
if(params!=null && params.length > 0){
for (int i = 0; i < params.length; i++) {
//占位符是从1开始的。参数的数组是从0开始的
preparedStatement.setObject(i+1,params[i] );
}
}
int row = preparedStatement.executeUpdate();
//5.释放资源
preparedStatement.close();
if(connection.getAutoCommit()){
JDBCUtilV2.release();
}
//6.返回结果
return row;
}
/**
* 通用的查询:多行多列、单行多列、单行单列
* 多行多列:List<Employee>
* 单行多列:Employee
* 单行单列:封装的是一个结果。Double、Integer、。。。。。
* 封装过程:
* 1、返回的类型:泛型:类型不确定,调用者知道,调用时,将此次查询的结果类型告知BaseDAO就可以了。
* 2、返回的结果:通用,List 可以存储多个结果,也可以存储一个结果 get(0)
* 3、结果的封装:反射,要求调用者告知BaseDAO要封装对象的类对象。 Class
*/ public <T> List<T> executeQuery(Class<T> clazz,String sql,Object... params)throws Exception{
//获取连接
Connection connection = JDBCUtilV2.getConnection();
//预编译SQL语句
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//设置占位符的值
if(params!=null && params.length > 0){
for (int i = 0; i < params.length; i++) {
preparedStatement.setObject(i+1, params[i]);
}
}
//执行SQL,并接受返回的结果集
ResultSet resultSet = preparedStatement.executeQuery();
//获取结果集中的元数据对象
//包含了:列的数量、每个列的名称
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
List<T> list = new ArrayList<>();
//处理结果
while(resultSet.next()){
//循环一次,代表有一行数据,通过反射创建一个对象
T t = clazz.newInstance();
//循环遍历当前行的列,循环几次,看有多少列
for (int i = 1; i <=columnCount ;i++){
//通过下表获取列的值
Object value = resultSet.getObject(i);
//获取到的列的value值,这个值就是t这个对象中的某一个属性
//获取当前拿到的列的名字 = 对象的属性名
String fieldName = metaData.getColumnLabel(i);
//通过类对象和fieldName获取要封装的对象的属性
Field field = clazz.getDeclaredField(fieldName);
//突破封装的private
field.setAccessible(true);
field.set(t,value);
}
list.add(t);
}
resultSet.close();
preparedStatement.close();
if(connection.getAutoCommit()){
JDBCUtilV2.release();
}
return list;
}
/**
* 通用查询:在上面查询的集合结果中获取第一个结果。 简化了获取单行单列的获取、单行多列的获取
*/
public <T> T executeQueryBean(Class<T> clazz,String sql,Object... params)throws Exception{
List<T> list = this.executeQuery(clazz, sql, params);
if(list ==null || list.size() == 0){
return null;
}
return list.get(0);
}
}
9.事务
9.1事务(Transaction)的概念:
事务是逻辑上的一组操作,要么全部执行,要么全不执行。它是数据库运行中的逻辑工作单位,由一个有限的数据库操作序列构成。
事务的特性(ACID):
- 原子性(Atomicity): 事务作为一个整体,不可分割。事务的所有操作要么全部成功,要么全部失败回滚。
- 一致性(Consistency): 事务执行前后,数据库都保持一致状态。所有约束都应该被保存。
- 隔离性(Isolation): 事务之间是相互隔离的,彼此不会相互影响。每个事务只能看到其他并行事务提交之前的数据。
- 持久性(Durability): 一旦事务提交成功,对数据的改变就是永久的,即使出现系统failure,也不会丢失。
事务的状态:
- 活跃(Active): 事务正在执行中的状态,更新会被暂存。
- 延迟(Partially Committed): 事务执行的最后一个语句时的状态。
- 提交(Committed): 事务中所有更新都已经写入数据库,不可逆。
- 回滚(Rollback): 撤销事务中所有更新操作,回到事务开始前的状态。
- 失败(Failed): 由于某种原因导致事务无法正常执行,如系统crash等。
事务的控制:
START TRANSACTION;
开始一个新事务。COMMIT;
提交当前事务,将数据更改持久化到数据库。ROLLBACK;
回滚当前事务,撤销所有未提交的更改。SAVEPOINT 保存点名;
在事务中创建一个保存点。ROLLBACK TO 保存点名;
回滚到保存点。
事务的隔离级别:
READ UNCOMMITTED
未提交读,最低隔离级别,可能读取"脏"数据。READ COMMITTED
提交读,避免"脏"数据,但有重复读和幻读问题。REPEATABLE READ
重复读,事务中多次读取相同,避免幻读。SERIALIZABLE
串行化,代价最高,完全避免并发问题。
9.2JDBC中事务实现的逻辑
try{
connection.setAutoCommit(false); //关闭自动提交了
//connection.setAutoCommit(false)也就类型于SET autocommit = off
//注意,只要当前connection对象,进行数据库操作,都不会自动提交事务
//数据库动作!
//prepareStatement - 单一的数据库动作 c r u d
//connection - 操作事务
//所有操作执行正确,提交事务!
connection.commit();
}catch(Execption e){
//出现异常,则回滚事务!
connection.rollback();
}
9.3JDBC事务代码实现
环境搭建:
-- 继续在rainsoul的库中创建银行表
CREATE TABLE t_bank(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键',
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
money INT UNSIGNED COMMENT '金额,不能为负值') ;
INSERT INTO t_bank(account,money) VALUES
('zhangsan',1000),('lisi',1000);
- Dao接口:
public interface BankDao{
int addMoney(Integer id,Integer money);
int subMoney(Integer id,Integer money);
}
- Dao接口实现类:
public class BankDaoImpl extends BaseDAO implements BankDao {
@Override
public int addMoney(Integer id, Integer money) {
try {
String sql = "UPDATE t_bank SET money = money + ? WHERE id = ?";
return executeUpdate(sql,money,id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public int subMoney(Integer id, Integer money) {
try {
String sql = "UPDATE t_bank SET money = money - ? WHERE id = ?";
return executeUpdate(sql,money,id);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
- 测试代码:
/**
* 测试银行转账的事务功能。
* 该方法模拟从一个账户(账号1)向另一个账户(账号2)转账100元的过程。
* 如果整个过程中发生异常,将进行事务回滚,确保数据一致性。
* 无参数和返回值。
*/
@Test
public void testTransaction(){
BankDao bankDao = new BankDaoImpl();
Connection connection=null;
try {
// 1. 获取数据库连接并开启事务
connection = JDBCUtilV2.getConnection();
connection.setAutoCommit(false); // 开启事务,将自动提交设置为false,以控制事务手动提交
// 2. 执行扣款操作
bankDao.subMoney(1,100);
int i = 10 / 0; // 模拟运行时异常
// 3. 执行加款操作
bankDao.addMoney(2,100);
// 4. 如果之前的操作没有异常,则提交事务
connection.commit();
} catch (Exception e) {
// 发生异常时,回滚事务
try {
connection.rollback();
} catch (Exception ex) {
// 抛出运行时异常,以便上层调用者能够处理
throw new RuntimeException(ex);
}
}finally {
// 释放数据库资源
JDBCUtilV2.release();
}
}
下一阶段:Javaweb。
转载自:https://juejin.cn/post/7357144204243796009