万字JDBC教程

学习地址\](【尚硅谷2024最新JDBC教程 \| jdbc基础到高级一套通关!】 [www.bilibili.com/video/BV1Tx...](https://link.juejin.cn?target=https%3A%2F%2Fwww.bilibili.com%2Fvideo%2FBV1Tx421S7HZ%2F%3Fp%3D34%26share_source%3Dcopy_web%26vd_source%3D1a47fe5dfa3ae49f76d25e6238dd7d03 "https://www.bilibili.com/video/BV1Tx421S7HZ/?p=34&share_source=copy_web&vd_source=1a47fe5dfa3ae49f76d25e6238dd7d03")) 建立项目如图: ![image.png](https://file.jishuzhan.net/article/1779450726364221442/6fda6cfd70b6f062b2a3fa052f33b3a2.webp) 并执行sql语句: ```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.快速入门 查询数据库所有信息: ```java 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(); } } ``` 查询结果: ![image.png](https://file.jishuzhan.net/article/1779450726364221442/6759cf820885899feb7dc8354c99a7a2.webp) # 3.核心API流程的理解 ## 3.1注册驱动 ```java 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") 这一行代码时,以下底层过程会依次发生: 类加载: 1. JVM接收到这个字符串字面量,根据它来查找并加载指定名称的类。类加载是通过类加载器(ClassLoader)体系实现的。通常情况下,系统类加载器(System ClassLoader)负责加载用户类路径(classpath)上的类。类加载包括三个阶段:加载(Loading)、验证(Verification)、准备(Preparation)。在这个过程中,JVM会查找指定类的.class文件(对于MySQL JDBC驱动来说,通常位于jar包中),将其字节码数据读入内存,并进行必要的安全性和正确性检查。 2. 类初始化:在类加载完成后,如果该类尚未被初始化,JVM会触发类的初始化过程。类初始化包括分配静态变量内存、执行静态初始化块(static {})中的代码等步骤。对于 com.mysql.cj.jdbc.Driver 类,其内部通常包含一个静态初始化块或静态方法,在类初次被加载时会被自动执行。这个初始化逻辑的核心任务是向Java的JDBC驱动管理器(java.sql.DriverManager)注册该驱动。 3. 驱动注册: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 就知道有哪些可用的驱动可以用来尝试建立数据库连接。 ![image.png](https://file.jishuzhan.net/article/1779450726364221442/fb6730013b748d6b746053c75cbb4788.webp) 我用个生活中的比喻解释一下: 想象你奶奶想打个电话,但是家里没有电话机。为了能打电话,首先需要去买一个电话机,把它正确安装好并插到电话线上。这相当于加载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得到结果。 示例代码: ```java 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 语句时,实际执行的是: ```sql SELECT * FROM users WHERE username='' OR '1'='1' ``` 由于`'1'='1'`这个条件永远为真,因此该查询会返回 users 表中的所有记录,而不是预期的基于用户名进行过滤的结果。 这种注入攻击发生的根本原因是,用户的输入数据被直接拼接到 SQL 语句中,而没有经过任何过滤和检查。攻击者可以构造特殊的字符串,影响原本的 SQL 语句执行逻辑,从而绕过认证或获取不应有的数据。 这种 SQL 注入攻击不仅可能导致数据泄露,还可能被利用执行任意的 SQL 语句,如删除或修改数据库数据等,造成严重的安全隐患。 因此,在使用 JDBC 时,应当避免使用`Statement`执行动态构建的 SQL 语句,而是使用`PreparedStatement`。`PreparedStatement`允许使用参数占位符,将参数数据与 SQL 语句分离,有效防止 SQL 注入攻击的发生。 dubug演示: ![image.png](https://file.jishuzhan.net/article/1779450726364221442/882da9940b791229b76c56b53d707589.webp) 代码: ```java 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 查询,作用如下: 1. 预编译SQL语句:在创建PreparedStatement时,就会预编译SQL语句,也就是SQL语句已经固定。 2. 防止SQL注入:`PreparedStatement` 支持参数化查询,将数据作为参数传递到SQL语句中,采用?占位符的方式,将传入的参数用一对单引号包裹起来'',无论传递什么都作为值。有效防止传入关键字或值导致SQL注入问题。 3. 性能提升:PreparedStatement是预编译SQL语句,同一SQL语句多次执行的情况下,可以复用,不必每次重新编译和解析。 ![image.png](https://file.jishuzhan.net/article/1779450726364221442/13cff60378ef949944fb35f4a92706c3.webp) 代码: ```java 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查询单行单列 ```java @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查询单行多列 ```java @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查询多行多列 ```java @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新增 ```java @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修改 ```java @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删除 ```java @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类: ![image.png](https://file.jishuzhan.net/article/1779450726364221442/e9bba544173fb0f0cf9eb60ff0df5bcd.webp) ```java //类名和数据库名对应,但是表名一般缩写,类名要全写! 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... } ``` 封装代码: ```java @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对象,此操作为主键回显。 ```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批量操作 一条一条操作很耗时间: ```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(); } ``` 优化后: ```java @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包。 ![image.png](https://file.jishuzhan.net/article/1779450726364221442/1d7634dde4983cb883a57cd8db47c49f.webp) ```java /** * 测试通过硬编码方式使用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(); } ``` 使用配置文件: ![image.png](https://file.jishuzhan.net/article/1779450726364221442/72859406e756d45a59542d864f92cace.webp) ```java /** * 测试通过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连接池的使用 ```java @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(); } ``` 使用配置文件: ![image.png](https://file.jishuzhan.net/article/1779450726364221442/3f802e2136c41ca581ded51c08a141f3.webp) ```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.创建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): 1. 维护一个连接池对象。 2. 对外提供在连接池中获取连接的方法 3. 对外提供回收连接的方法 注意:**工具类仅对外提供共性的功能代码,所以方法均为静态方法!** 这段代码实现了一个JDBC工具类 `JDBCUtil`,用于管理数据库连接池和获取/释放连接。我将为每部分代码添加注释进行解释。 ```java 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); } } } ``` 这个工具类主要实现了以下功能: 1. 在静态代码块中,通过读取配置文件 `db.properties` 获取数据库连接参数,并使用 Druid 连接池工厂类创建一个连接池对象 `dataSource`。 2. 提供一个静态方法 `getConnection()`,用于从连接池中获取一个数据库连接。 3. 提供一个静态方法 `release(Connection connection)`,用于关闭指定的数据库连接,将其归还到连接池中。 使用这个工具类,可以方便地获取和释放数据库连接,同时利用连接池技术提高连接的复用率和性能。需要注意的是,在项目启动时就创建了连接池对象,并在静态代码块中加载了配置文件,这种方式确保了连接池的初始化只执行一次。 ## 7.2ThreadLocal 用一个生活中的例子来解释什么是`ThreadLocal`以及它的使用场景。 我们可以把`ThreadLocal`想象成一个小柜子,每个人都有自己专属的小柜子。这个小柜子就相当于每个线程内部的存储空间。 假设你奶奶有几个孙子孙女,他们都很顽皮,经常在家里到处乱跑。为了防止他们弄乱房间,你给每个孩子准备了一个专属的小柜子,让他们把自己的东西放进去。 这样一来,每个孩子拿自己柜子里的东西就不会影响其他孩子,也不会把房间弄乱了。这就是`ThreadLocal`的作用,它为每个"线程"提供了一个独立的存储空间,可以在里面放自己的东西,而不会影响到其他"线程"。 在程序中,多个线程就像你奶奶家的这些孩子一样,他们并发地运行,有可能会争夺一些共享的资源(比如数据库连接对象)。如果多个线程使用同一个资源,就可能会导致线程安全问题。 通过`ThreadLocal`,每个线程就拥有了自己独立的"小柜子",可以放置自己的连接对象。当线程需要使用连接时,从自己的"小柜子"里取出来用;用完后,又放回到"小柜子"中,不会影响其他线程。 这种做法避免了线程之间相互影响和频繁地从连接池中获取连接,提高了程序的运行效率和线程安全性。 当然,我们要记得在线程结束后,将"小柜子"里的东西清理干净,避免浪费资源。否则就像孩子长大后忘记把自己的旧东西清理出柜子一样,会导致资源的浪费和积累。 总之,`ThreadLocal`就像是为每个线程准备了一个"独立的小空间",保证了线程之间使用共享资源时的隔离性和安全性。只要用好了,就可以让程序运行得更高效和可靠。 ThreadLocal的相关知识点整理如下: 1. **ThreadLocal概念** * ThreadLocal是JDK包中的一个类,可以在同一个线程内创建独立的副本变量。 * 每个线程都会有一个自己独立的副本变量,线程之间彼此不会相互影响。 * ThreadLocal提供get()、set(value)、remove()等方法访问和修改副本变量。 2. **ThreadLocal原理** * 每个Thread内部都有一个ThreadLocalMap类型的成员变量,存储当前线程的副本变量。 * ThreadLocal作为Map的键(key),副本变量作为Map的值(value)存储。 * 每个线程读写自己所属线程的变量副本,线程之间相互隔离。 3. **ThreadLocal使用场景** * 线程本地存储(每个线程内部有自己独立的变量副本) * 事务管理(绑定事务上下文) * 日志记录(MDC机制存储日志跟踪信息) * 数据库连接池(每个线程有独立的连接对象) 4. **ThreadLocal使用注意事项** * 每个线程自己使用后,需要调用remove()清除副本,避免内存泄漏。 * 副本变量是存储在线程对象内部的Map中,生存周期随线程终止而销毁。 * 减小副本变量的范围,使用后再强制设为null,避免副本变量带来的额外内存占用。 * 只有在状态确实需要与线程相关联时,才使用ThreadLocal存储状态。 5. **ThreadLocal应用实例** * JDBC中使用ThreadLocal获取线程独立的Connection对象 * Servlet组件绑定和传递Request、Response等对象 * 实现简化的线程上下文管理器(TransmittableThreadLocal) 6. **ThreadLocal内存泄漏风险** * 由于ThreadLocalMap的生存周期是与线程一直存在的,若不手动remove,就会导致内存泄漏。 * 使用弱引用或定期删除无效线程变量,防止内存泄漏。 * 在线程池中注意每次运行结束后,清理ThreadLocal存储。 ## JDBCUtilV2工具类(V2.0) 这个 `JDBCUtilV2` 类是在原有 `JDBCUtil` 类的基础上进行了改进,增加了 `ThreadLocal` 的支持。我将为这部分新增的代码添加注释进行解释。 ```java public class JDBCUtilV2 { // ... // 创建一个ThreadLocal对象,用于存储线程级别的Connection对象 private static ThreadLocal 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` 对象。具体来说: 1. 声明了一个静态的 `ThreadLocal` 对象 `threadLocal`。 2. 在 `getConnection()` 方法中,首先尝试从当前线程的 `ThreadLocal` 中获取 `Connection` 对象。如果获取不到,则从连接池中获取一个新的连接,并将其绑定到当前线程的 `ThreadLocal` 中。 3. 在 `release()` 方法中,从当前线程的 `ThreadLocal` 中获取绑定的 `Connection` 对象。如果存在,则先将其从 `ThreadLocal` 中移除,然后将连接的自动提交设置为 `true`(如果之前手动设置过),最后将连接归还给连接池。 使用 `ThreadLocal` 的好处是每个线程都可以独立获取和使用自己的 `Connection` 对象,避免了多线程环境下的线程安全问题。同时,在操作完成后,通过调用 `release()` 方法,可以将 `Connection` 对象正确地归还给连接池,实现了连接的复用。 需要注意的是,在使用 `ThreadLocal` 时,必须确保在线程结束后正确地释放资源,否则可能会导致内存泄漏。在这个例子中,由于 `ThreadLocal` 对象是静态的,所以需要通过其他方式(如在Web应用中监听`ServletContextListener`事件)来清理 `ThreadLocal`。 ![image.png](https://file.jishuzhan.net/article/1779450726364221442/ac7c03055f6ab3f9a0ee999c8fe07fa5.webp) 拿到同一个。 # 8.DAO封装及BaseDAO工具类 DAO模式的几个核心点: 1. **职责划分**:DAO层专注于对数据库的访问操作,不涉及业务逻辑,将数据库操作与业务逻辑解耦。业务逻辑由Service层负责处理。 2. **面向对象**:Java天生面向对象,一个DAO对象对应一张数据库表,每个DAO对象维护这张表的CRUD(增删改查)操作方法。 3. **封装性**:DAO对象对外提供标准化的数据访问API接口,上层模块无需关心数据库访问的具体实现细节。 4. **易于维护**:数据库操作均集中在DAO层,如果需要更换底层数据存储方式,只需修改DAO层即可,上层代码无需改动。 5. **复用性**:通用的数据访问方法可以在DAO层得到复用,提高代码的重用率。 6. **规范性**:引入DAO层有利于遵循设计模式规范,促进项目结构清晰和可维护性。 基本上每一个数据表都应该有一个对应的DAO接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些DAO的实现类可以抽取一个公共的父类,复用增删改查的基本操作,我们称为BaseDAO。 ## 8.1创建员工DAO接口 ```java /** * EmployeeDao这个类对应的是t_emp这张表的增删改查的操作 */ public interface EmployeeDao { /** * 数据库对应的查询所有的操作 * @return 表中所有的数据 */ List 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); } ``` ```java public class EmployeeDaoImpl extends BaseDAO implements EmployeeDao { @Override public List 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搭建 ```java /** * 将共性的数据库的操作代码封装在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 * 单行单列:封装的是一个结果。Double、Integer、。。。。。 * 封装过程: * 1、返回的类型:泛型:类型不确定,调用者知道,调用时,将此次查询的结果类型告知BaseDAO就可以了。 * 2、返回的结果:通用,List 可以存储多个结果,也可以存储一个结果 get(0) * 3、结果的封装:反射,要求调用者告知BaseDAO要封装对象的类对象。 Class */ public List executeQuery(Class 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 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 executeQueryBean(Class clazz,String sql,Object... params)throws Exception{ List list = this.executeQuery(clazz, sql, params); if(list ==null || list.size() == 0){ return null; } return list.get(0); } } ``` # 9.事务 ## 9.1事务(Transaction)的概念: 事务是逻辑上的一组操作,要么全部执行,要么全不执行。它是数据库运行中的逻辑工作单位,由一个有限的数据库操作序列构成。 **事务的特性(ACID)**: 1. **原子性(Atomicity)**: 事务作为一个整体,不可分割。事务的所有操作要么全部成功,要么全部失败回滚。 2. **一致性(Consistency)**: 事务执行前后,数据库都保持一致状态。所有约束都应该被保存。 3. **隔离性(Isolation)**: 事务之间是相互隔离的,彼此不会相互影响。每个事务只能看到其他并行事务提交之前的数据。 4. **持久性(Durability)**: 一旦事务提交成功,对数据的改变就是永久的,即使出现系统failure,也不会丢失。 **事务的状态**: 1. **活跃(Active)**: 事务正在执行中的状态,更新会被暂存。 2. **延迟(Partially Committed)**: 事务执行的最后一个语句时的状态。 3. **提交(Committed)**: 事务中所有更新都已经写入数据库,不可逆。 4. **回滚(Rollback)**: 撤销事务中所有更新操作,回到事务开始前的状态。 5. **失败(Failed)**: 由于某种原因导致事务无法正常执行,如系统crash等。 **事务的控制**: * `START TRANSACTION;`开始一个新事务。 * `COMMIT;` 提交当前事务,将数据更改持久化到数据库。 * `ROLLBACK;` 回滚当前事务,撤销所有未提交的更改。 * `SAVEPOINT 保存点名;` 在事务中创建一个保存点。 * `ROLLBACK TO 保存点名;` 回滚到保存点。 **事务的隔离级别**: * `READ UNCOMMITTED` 未提交读,最低隔离级别,可能读取"脏"数据。 * `READ COMMITTED` 提交读,避免"脏"数据,但有重复读和幻读问题。 * `REPEATABLE READ` 重复读,事务中多次读取相同,避免幻读。 * `SERIALIZABLE` 串行化,代价最高,完全避免并发问题。 ## 9.2JDBC中事务实现的逻辑 ```java 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事务代码实现 环境搭建: ```sql -- 继续在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); ``` ![image.png](https://file.jishuzhan.net/article/1779450726364221442/6df2f13e890746d3b7ca208d6ba8937c.webp) 1. Dao接口: ```java public interface BankDao{ int addMoney(Integer id,Integer money); int subMoney(Integer id,Integer money); } ``` 2. Dao接口实现类: ```java 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); } } } ``` 3. 测试代码: ```java /** * 测试银行转账的事务功能。 * 该方法模拟从一个账户(账号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。

相关推荐
铲子Zzz10 分钟前
Java使用接口AES进行加密+微信小程序接收解密
java·开发语言·微信小程序
霖檬ing15 分钟前
K8s——配置管理(1)
java·贪心算法·kubernetes
float_六七35 分钟前
SQL六大核心类别全解析
数据库·sql·oracle
Vic101011 小时前
Java 开发笔记:多线程查询逻辑的抽象与优化
java·服务器·笔记
Biaobiaone1 小时前
Java中的生产消费模型解析
java·开发语言
特立独行的猫a1 小时前
11款常用C++在线编译与运行平台推荐与对比
java·开发语言·c++
louisgeek2 小时前
Java 位运算
java
Code季风2 小时前
将 gRPC 服务注册到 Consul:从配置到服务发现的完整实践(上)
数据库·微服务·go·json·服务发现·consul
hweiyu002 小时前
Maven 私库
java·maven
Boilermaker19922 小时前
【Java EE】SpringIoC
前端·数据库·spring