JDBC也即Java DataBase Connectivity的缩写,表示Java数据库连接:用Java语言向数据库发送SQL语句来操作数据库。
JDBC其实就是一组由SUN公司制定的规范(一组接口)。各个数据库厂商遵循该规范并编写相关的实现类(这里的实现类被称为驱动,各个数据库厂商提供的驱动不同),其他程序员只需将这些实现类导入自己的相关程序(如何导入请自己查询classpath的相关知识),并面向接口编程,即可访问数据库。
由此可以提炼出三个角色:
JDBC规范所在的包(JDK里面):java.sql.
MySQL驱动的下载链接:MySQL :: Download Connector/J(可以通过Maven管理)
DROP DATABASE IF EXISTS jdbc;
CREATE DATABASE jdbc;
USE jdbc;
CREATE TABLE t_employee (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
age INT,
gender VARCHAR(1),
salary DECIMAL(10, 2),
hiredate DATE,
tel VARCHAR(11)
);
INSERT INTO t_employee (name, age, gender, salary, hiredate, tel)
VALUES
('张三', 30, '男', 8500.0, '2023-10-25', '13870092312'),
('李四', 28, '男', 7900.0, '2024-11-15', '15809081746');
作用一:将 JDBC 驱动程序(实现类)从硬盘上的文件系统中加载到内存中。
作用二:使得 DriverManager 可以通过一个统一的接口来管理该驱动程序的所有连接操作。
第一种方法:
Driver driver = new com.mysql.cj.jdbc.Driver(); // 等号左边的Driver是JDK里的接口(需要导包),右边的Driver是厂商编写的实现类(用全类名导入)
DriverManager.registerDriver(driver); // 注意该方法有异常抛出
合二为一:DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
第二种方法(推荐):
Class.forName("com.mysql.cj.jdbc.Driver");//原理:在驱动的com.mysql.cj.jdbc.Driver类中已经在静态代码块中实现了第一种方法的代码,因此只需将该类加载到JVM中,就会自动完成注册
第三种方法:
在MySQL 5及以上版本中,可省略驱动注册步骤。原理:在驱动的‘META-INF/services’目录下有‘java.sql.Driver’配置文件,指定了‘com.mysql.cj.jdbc.Driver’,系统通过Java SPI机制自动完成注册。但实际开发中,部分数据库驱动不支持自动发现,仍需手动注册。因此建议保留手动注册步骤,以确保兼容性和代码健壮性。
#准备工作:为遵循 OCP 开闭原则,建议将数据库连接信息配置至属性文件。需在.properties配置文件中添加以下内容:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jdbc
#注意:jdbc.url值中的jdbc为目标数据库名称。URL 后可通过?拼接传输参数(键值对间用&分隔),例如设置字符集时可写成?useUnicode=true&characterEncoding=utf8。
jdbc.username=root
jdbc.password=123456
作用:获取java.sql.Connection对象,该对象的创建标志着mysql进程和jvm进程之间的通道打开了
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
//String driver = resourceBundle.getString("jdbc.driver");用于改变第一步:Class.forName(driver);
String url = resourceBundle.getString("jdbc.url");
String user = resourceBundle.getString("jdbc.user");
String password = resourceBundle.getString("jdbc.password");
Connection connection = DriverManager.getConnection(url, user, password);//注意getConnection()方法有重载
作用:获取java.sql.Statement对象,该对象负责将SQL语句发送给数据库,数据库负责执行该SQL语句
Statement statement = connection.createStatement();
作用:执行具体的SQL语句,例如:insert delete update select等。
String sqlSelect = "select id,name,age,gender,salary,hiredate,tel from t_employee";
resultSet = statement.executeQuery(sqlSelect);
前提:如果之前的操作是DQL查询语句,才会有处理查询结果集这一步。
作用:执行DQL语句通常会返回查询结果集对象:java.sql.ResultSet。对于ResultSet查询结果集来说,通常的操作是针对查询结果集进行结果集的遍历。
第一种方法:通过列名获取,但如果SQL语句中给列名取了别名,那应该用别名获取
while (resultSet.next()) {
String id = resultSet.getString("id");
String name = resultSet.getString("name");
String age = resultSet.getString("age");
String gender = resultSet.getString("gender");
String salary = resultSet.getString("salary");
String hiredate = resultSet.getString("hiredate");
String tel = resultSet.getString("tel");
System.out.println("id:" + id + "\tname:" + name + "\tage:" + age + "\tgender:" + gender + "\tsalary:" + salary + "\thiredate:" + hiredate + "\ttel:" + tel);
}
第二种方法:通过第几列进行,列数从1开始递增
while (resultSet.next()) {
String id = resultSet.getString(1);
String name = resultSet.getString(2);
String age = resultSet.getString(3);
String gender = resultSet.getString(4);
String salary = resultSet.getString(5);
String hiredate = resultSet.getString(6);
String tel = resultSet.getString(7);
System.out.println("id:" + id + "\tname:" + name + "\tage:" + age + "\tgender:" + gender + "\tsalary:" + salary + "\thiredate:" + hiredate + "\ttel:" + tel);
}
第三种方法:通过具体的数据类型进行
while (resultSet.next()) {
Long id = resultSet.getLong("id");
String name = resultSet.getString("name");
System.out.println("id:" + id + "\tname:" + name);
}
作用一:避免资源浪费:数据库连接属于稀缺资源,不释放会导致连接池耗尽或 MySQL 进程资源占用持续累积。
作用二:防止内存泄漏:Java 垃圾回收机制仅回收无引用对象,但 JDBC 资源(如数据库连接)属于底层系统资源,需手动调用close()方法才能真正释放,否则会导致内存泄漏。
第一种方法:传统手动关闭(需遵循关闭顺序)
原则一:先打开的资源后关闭(如ResultSet→Statement→Connection)。
原则二:每个资源单独使用try-catch,避免因某资源关闭失败影响后续操作,通常放在finally代码块中。
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
第二种方法:Java 7 Try-with-resources 自动释放(推荐)
优势一:自动管理资源生命周期,无需手动调用close(),代码更简洁且避免漏关问题。
优势二:支持实现AutoCloseable接口的资源(如Connection、Statement、ResultSet)。
try (
Connection connection = DriverManager.getConnection(url, user, password);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM table");
) {
// 业务逻辑代码
} catch (SQLException e) {
e.printStackTrace();
}
// 无需手动关闭资源,try代码块结束后自动调用close()
package xyz.foragain.sectionone;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ResourceBundle;
import java.util.Scanner;
public class InsertJDBC {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入新增成员的name:");
String name = scanner.nextLine();
System.out.print("请输入新增成员的age:");
Integer age = scanner.nextInt();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入新增成员的gender:");
String gender = scanner.nextLine();
System.out.print("请输入新增成员的salary:");
Double salary = scanner.nextDouble();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入新增成员的hiredate(格式: yyyy-MM-dd):");
String hiredate = scanner.nextLine();
System.out.print("请输入新增成员的tel:");
String tel = scanner.nextLine();
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
String driver = resourceBundle.getString("jdbc.driver");
String url = resourceBundle.getString("jdbc.url");
String username = resourceBundle.getString("jdbc.username");
String password = resourceBundle.getString("jdbc.password");
String sql = "insert into t_employee (name,age,gender,salary,hiredate,tel) values ('" + name + "'," + age + ",'" + gender + "'," + salary + ",'" + hiredate + "','" + tel + "')";
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
System.err.println("找不到JDBC驱动类: " + e.getMessage());
return;
}
try (Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement()) {
int count = statement.executeUpdate(sql);
System.out.println("新增了" + count + "条数据");
} catch (SQLException e) {
System.err.println("执行SQL语句时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
package xyz.foragain.sectionone;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ResourceBundle;
import java.util.Scanner;
public class DeleteJDBC {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入要删除成员的id:");
Long id = scanner.nextLong();
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
String driver = resourceBundle.getString("jdbc.driver");
String url = resourceBundle.getString("jdbc.url");
String username = resourceBundle.getString("jdbc.username");
String password = resourceBundle.getString("jdbc.password");
String sql = "delete from t_employee where id = " + id;
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
System.err.println("找不到JDBC驱动类: " + e.getMessage());
return;
}
try (Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement()) {
int count = statement.executeUpdate(sql);
System.out.println("删除了" + count + "条数据");
} catch (SQLException e) {
System.err.println("执行SQL语句时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
package xyz.foragain.sectionone;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ResourceBundle;
import java.util.Scanner;
public class UpdateJDBC {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入要更改成员的id:");
Long id = scanner.nextLong();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入要更改成员的name:");
String name = scanner.nextLine();
System.out.print("请输入要更改成员的age:");
Integer age = scanner.nextInt();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入要更改成员的gender:");
String gender = scanner.nextLine();
System.out.print("请输入要更改成员的salary:");
Double salary = scanner.nextDouble();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入要更改成员的hiredate(格式: yyyy-MM-dd):");
String hiredate = scanner.nextLine();
System.out.print("请输入要更改成员的tel:");
String tel = scanner.nextLine();
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
String driver = resourceBundle.getString("jdbc.driver");
String url = resourceBundle.getString("jdbc.url");
String username = resourceBundle.getString("jdbc.username");
String password = resourceBundle.getString("jdbc.password");
String sql = "update t_employee set name = '" + name + "',age =" + age + ",gender='" + gender + "',salary=" + salary + ",hiredate='" + hiredate + "',tel='" + tel + "'where id =" + id;
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
System.err.println("找不到JDBC驱动类: " + e.getMessage());
return;
}
try (Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement()) {
int count = statement.executeUpdate(sql);
System.out.println("更改了" + count + "条数据");
} catch (SQLException e) {
System.err.println("执行SQL语句时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
package xyz.foragain.sectionone;
import java.sql.*;
import java.util.ResourceBundle;
public class SelectJDBC {
public static void main(String[] args) {
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
String driver = resourceBundle.getString("jdbc.driver");
String url = resourceBundle.getString("jdbc.url");
String username = resourceBundle.getString("jdbc.username");
String password = resourceBundle.getString("jdbc.password");
String sql = "SELECT id,name,age,gender,salary,hiredate,tel from t_employee";
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
System.err.println("找不到JDBC驱动类: " + e.getMessage());
return;
}
try (Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
while (resultSet.next()) {
String id = resultSet.getString("id");
String name = resultSet.getString("name");
String age = resultSet.getString("age");
String gender = resultSet.getString("gender");
String salary = resultSet.getString("salary");
String hiredate = resultSet.getString("hiredate");
String tel = resultSet.getString("tel");
System.out.println("id:" + id + "\tname:" + name + "\tage:" + age + "\tgender:" + gender + "\tsalary:" + salary + "\thiredate:" + hiredate + "\ttel:" + tel);
}
} catch (SQLException e) {
System.err.println("执行SQL语句时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
package xyz.foragain.sectionone;
import java.sql.*;
import java.util.ResourceBundle;
import java.util.Scanner;
public class GetInsertKeyJDBC {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入新增成员的name:");
String name = scanner.nextLine();
System.out.print("请输入新增成员的age:");
Integer age = scanner.nextInt();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入新增成员的gender:");
String gender = scanner.nextLine();
System.out.print("请输入新增成员的salary:");
Double salary = scanner.nextDouble();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入新增成员的hiredate(格式: yyyy-MM-dd):");
String hiredate = scanner.nextLine();
System.out.print("请输入新增成员的tel:");
String tel = scanner.nextLine();
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
String driver = resourceBundle.getString("jdbc.driver");
String url = resourceBundle.getString("jdbc.url");
String username = resourceBundle.getString("jdbc.username");
String password = resourceBundle.getString("jdbc.password");
String sql = "insert into t_employee (name,age,gender,salary,hiredate,tel) values ('" + name + "'," + age + ",'" + gender + "'," + salary + ",'" + hiredate + "','" + tel + "')";
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
System.err.println("找不到JDBC驱动类: " + e.getMessage());
return;
}
try (Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement()) {
int count = statement.executeUpdate(sql, statement.RETURN_GENERATED_KEYS);//使用executeUpdate()重载方法,传入一个标志位
System.out.println("新增了" + count + "条数据");
ResultSet resultSet = statement.getGeneratedKeys();//调用数据库操作对象的getGeneratedKeys()方法获得一个包含插入行主键值的 ResultSet 对象
if (resultSet.next()) {
long id = resultSet.getLong(1);//MySQL数据库这里最好用第几列来获取,不要用列名,否则会报错。可见主键值的获取方式具有一定的差异,需要根据不同的数据库种类和版本来进行调整。
System.out.println("新增数据行主键为:" + id);
}
} catch (SQLException e) {
System.err.println("执行SQL语句时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
package xyz.foragain.sectionone;
import java.sql.*;
import java.util.ResourceBundle;
public class GetMetaJDBC {
public static void main(String[] args) {
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
String driver = resourceBundle.getString("jdbc.driver");
String url = resourceBundle.getString("jdbc.url");
String username = resourceBundle.getString("jdbc.username");
String password = resourceBundle.getString("jdbc.password");
String sql = "SELECT id,name,age,gender,salary,hiredate,tel from t_employee";
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
System.err.println("找不到JDBC驱动类: " + e.getMessage());
return;
}
try (Connection connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
ResultSetMetaData metaData = resultSet.getMetaData();//通过结果集对象获得元数据集对象
for (int i = 1; i <= metaData.getColumnCount(); i++) {//通过元数据集获取表中列的总数
//进一步获取列的信息
System.out.println("列名:" + metaData.getColumnName(i) + ",数据类型:" + metaData.getColumnTypeName(i) + ",列的长度:" + metaData.getColumnDisplaySize(i));
}
} catch (SQLException e) {
System.err.println("执行SQL语句时发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
**步骤:**键盘读入=>读取配置文件信息=>编写SQL语句=>JDBC编程六步走(注册驱动=>数据库连接对象=>数据库操作对象=>执行SQL语句=>处理结果集对象=>释放资源)
从第二章内容可知,文中提及的 SQL 语句采用了拼接操作。在实际软件开发场景中,通常由前端向服务器发送请求,后端接收到请求信息后进行处理。以查询用户信息为例,当前端需要获取某人的详细资料时,会将该用户的姓名作为参数发送至后端。后端接收到姓名参数后,会将其拼接到 SQL 查询语句中,通过执行该语句从数据库中检索出对应的完整用户信息,最终将查询结果封装为响应数据返回给前端。
那么直接拼接操作会导致什么后果呢?现在有一段需要拼接的SQL语句如下:
select * from t_user where name = 用户输入的用户名 and password = 用户输入的密码;
该SQL对应的后端拼接代码为:
String sql = "select * from t_user where name = '" + name + "'and password = '" + password + "'";
按道理来说:用户名和密码是正确的,则查询成功。如果不对,则查询失败。
现在有某个人也想查询相关信息,可是他没有对应的用户名和密码,那么他能够查询成功吗?他输入的相关值如下:
用户名(也即name的值):随便写
密码(也即password的值):随便写' or '1'='1
该值与后端对应的SQL语句拼接后代码如下:
String sql = "select * from t_user where name = '随便写' and password = '随便写' or '1'='1'";
可以发现条件变了,不单单只判断用户名和密码符合条件,还有判断1是否等于1,1=1是肯定正确的,再加上连接关系是or,显然符合添加,查询成功!!!以上操作即可称为SQL注入。
以上操作明显违背了条件查询的初衷,设置出条件查询本来就是为了过滤出相关人员,如果不相关人员也可以随意查询,这明显是一个及其危险的操作。
SQL注入是Statement接口造成的。Statement执行原理是:先进行字符串的拼接,再将拼接好的SQL语句发送给数据库服务器,数据库服务器进行SQL语句的编译,然后执行。因此用户提供的信息中如果含有SQL语句的关键字,那么这些关键字可以参加SQL语句的编译,导致原SQL语句被扭曲。(先拼接后编译)
为了解决SQL注入,JDBC引入了一个新的接口PreparedStatement(预编译的数据库操作对象)。PreparedStatement是Statement接口的子接口。它俩是继承关系。PreparedStatement执行原理是:先对SQL语句进行预先的编译,然后再向SQL语句指定的位置传值,也就是说:用户提供的信息中即使含有SQL语句的关键字,那么这个信息也只会被当做一个值传递给SQL语句,用户提供的信息不再参与SQL语句的编译了,这样就解决了SQL注入问题。(先编译后拼接)
setString()
强制校验字符串类型),避免因参数类型不匹配导致的运行时错误,而 Statement 因采用字符串拼接,无法在编译阶段检测类型问题,存在潜在异常风险。从第二章可以发现:增删改查的重复代码太多,因此可以提取出一个工具类供使用
package xyz.foragain.sectiontwo;
import java.sql.*;
import java.util.ResourceBundle;
public class JDBCUtils {
private static String url;
private static String username;
private static String password;
static {
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
String driver = resourceBundle.getString("jdbc.driver");
url = resourceBundle.getString("jdbc.url");
username = resourceBundle.getString("jdbc.username");
password = resourceBundle.getString("jdbc.password");
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
System.err.println("找不到JDBC驱动类: " + e.getMessage());
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
//由于PreparedStatement是Statement接口的子接口,所以PreparedStatement型也能传入该方法
public static void close(Connection connection, Statement statement, ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
System.err.println("关闭ResultSet失败: " + e.getMessage());
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
System.err.println("关闭Statement失败: " + e.getMessage());
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
System.err.println("关闭Connection失败: " + e.getMessage());
}
}
}
}
package xyz.foragain.sectiontwo;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Scanner;
public class InsertJDBC {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入新增成员的name:");
String name = scanner.nextLine();
System.out.print("请输入新增成员的age:");
Integer age = scanner.nextInt();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入新增成员的gender:");
String gender = scanner.nextLine();
System.out.print("请输入新增成员的salary:");
Double salary = scanner.nextDouble();
scanner.nextLine();//消耗掉换行符
LocalDate hiredate = null;
while (hiredate == null) {
System.out.print("请输入新增成员的hiredate(格式: yyyy-MM-dd):");
String inputHiredate = scanner.nextLine();
try {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
hiredate = LocalDate.parse(inputHiredate, dateTimeFormatter);
} catch (DateTimeParseException e) {
System.out.println("日期格式不正确,请使用yyyy-MM-dd格式!");
}
}
System.out.print("请输入新增成员的tel:");
String tel = scanner.nextLine();
String sql = "insert into t_employee (name,age,gender,salary,hiredate,tel) values (?,?,?,?,?,?)";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection();
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, name);//给第一个?传值
preparedStatement.setInt(2, age);//给第二个?传值
preparedStatement.setString(3, gender);//……
preparedStatement.setDouble(4, salary);
preparedStatement.setDate(5, Date.valueOf(hiredate));
preparedStatement.setString(6, tel);
int count = preparedStatement.executeUpdate();
System.out.println("新增了" + count + "条数据");
} catch (SQLException e) {
System.err.println("SQL执行异常: " + e.getMessage());
e.printStackTrace(); // 打印详细堆栈信息,便于调试
} finally {
JDBCUtils.close(connection, preparedStatement, null);
}
}
}
package xyz.foragain.sectiontwo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Scanner;
public class DeleteJDBC {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入要删除成员的id:");
Long id = scanner.nextLong();
String sql = "delete from t_employee where id = ?";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection();
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setLong(1, id);
int count = preparedStatement.executeUpdate();
System.out.println("删除了" + count + "条数据");
} catch (SQLException e) {
System.err.println("SQL执行异常: " + e.getMessage());
e.printStackTrace(); // 打印详细堆栈信息,便于调试
} finally {
JDBCUtils.close(connection, preparedStatement, null);
}
}
}
package xyz.foragain.sectiontwo;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Scanner;
public class UpdateJDBC {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入要更改成员的id:");
Long id = scanner.nextLong();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入要更改成员的name:");
String name = scanner.nextLine();
System.out.print("请输入要更改成员的age:");
Integer age = scanner.nextInt();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入要更改成员的gender:");
String gender = scanner.nextLine();
System.out.print("请输入要更改成员的salary:");
Double salary = scanner.nextDouble();
scanner.nextLine();//消耗掉换行符
LocalDate hiredate = null;
while (hiredate == null) {
System.out.print("请输入新增成员的hiredate(格式: yyyy-MM-dd):");
String inputHiredate = scanner.nextLine();
try {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
hiredate = LocalDate.parse(inputHiredate, dateTimeFormatter);
} catch (DateTimeParseException e) {
System.out.println("日期格式不正确,请使用yyyy-MM-dd格式!");
}
}
System.out.print("请输入要更改成员的tel:");
String tel = scanner.nextLine();
String sql = "update t_employee set name = ? , age = ? , gender = ? , salary = ? , hiredate = ? , tel = ? where id = ?";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection();
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, name);
preparedStatement.setInt(2, age);
preparedStatement.setString(3, gender);
preparedStatement.setDouble(4, salary);
preparedStatement.setDate(5, Date.valueOf(hiredate));
preparedStatement.setString(6, tel);
preparedStatement.setLong(7, id);
int count = preparedStatement.executeUpdate();
System.out.println("更改了" + count + "条数据");
} catch (SQLException e) {
System.err.println("SQL执行异常: " + e.getMessage());
e.printStackTrace(); // 打印详细堆栈信息,便于调试
} finally {
JDBCUtils.close(connection, preparedStatement, null);
}
}
}
package xyz.foragain.sectiontwo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class SelectJDBC {
public static void main(String[] args) {
String sql = "SELECT id,name,age,gender,salary,hiredate,tel from t_employee";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
connection = JDBCUtils.getConnection();
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String id = resultSet.getString("id");
String name = resultSet.getString("name");
String age = resultSet.getString("age");
String gender = resultSet.getString("gender");
String salary = resultSet.getString("salary");
String hiredate = resultSet.getString("hiredate");
String tel = resultSet.getString("tel");
System.out.println("id:" + id + "\tname:" + name + "\tage:" + age + "\tgender:" + gender + "\tsalary:" + salary + "\thiredate:" + hiredate + "\ttel:" + tel);
}
} catch (SQLException e) {
System.err.println("SQL执行异常: " + e.getMessage());
e.printStackTrace(); // 打印详细堆栈信息,便于调试
} finally {
JDBCUtils.close(connection, preparedStatement, resultSet);
}
}
}
package xyz.foragain.sectiontwo;
import java.sql.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Scanner;
public class GetInsertKeyJDBC {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入新增成员的name:");
String name = scanner.nextLine();
System.out.print("请输入新增成员的age:");
Integer age = scanner.nextInt();
scanner.nextLine();//消耗掉换行符
System.out.print("请输入新增成员的gender:");
String gender = scanner.nextLine();
System.out.print("请输入新增成员的salary:");
Double salary = scanner.nextDouble();
scanner.nextLine();//消耗掉换行符
LocalDate hiredate = null;
while (hiredate == null) {
System.out.print("请输入新增成员的hiredate(格式: yyyy-MM-dd):");
String inputHiredate = scanner.nextLine();
try {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
hiredate = LocalDate.parse(inputHiredate, dateTimeFormatter);
} catch (DateTimeParseException e) {
System.out.println("日期格式不正确,请使用yyyy-MM-dd格式!");
}
}
System.out.print("请输入新增成员的tel:");
String tel = scanner.nextLine();
String sql = "insert into t_employee (name,age,gender,salary,hiredate,tel) values (?,?,?,?,?,?)";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection();
preparedStatement = connection.prepareStatement(sql, preparedStatement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, name);
preparedStatement.setInt(2, age);
preparedStatement.setString(3, gender);
preparedStatement.setDouble(4, salary);
preparedStatement.setDate(5, Date.valueOf(hiredate));
preparedStatement.setString(6, tel);
int count = preparedStatement.executeUpdate();
System.out.println("新增了" + count + "条数据");
ResultSet resultSet = preparedStatement.getGeneratedKeys();
if (resultSet.next()) {
long id = resultSet.getLong(1);
System.out.println("新增数据行主键为:" + id);
}
} catch (SQLException e) {
System.err.println("SQL执行异常: " + e.getMessage());
e.printStackTrace(); // 打印详细堆栈信息,便于调试
} finally {
JDBCUtils.close(connection, preparedStatement, null);
}
}
}
package xyz.foragain.sectiontwo;
import java.sql.*;
public class GetMetaJDBC {
public static void main(String[] args) {
String sql = "SELECT id,name,age,gender,salary,hiredate,tel from t_employee";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
connection = JDBCUtils.getConnection();
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
ResultSetMetaData metaData = resultSet.getMetaData();
for (int i = 1; i <= metaData.getColumnCount(); i++) {
System.out.println("列名:" + metaData.getColumnName(i) + ",数据类型:" + metaData.getColumnTypeName(i) + ",列的长度:" + metaData.getColumnDisplaySize(i));
}
} catch (SQLException e) {
System.err.println("SQL执行异常: " + e.getMessage());
e.printStackTrace(); // 打印详细堆栈信息,便于调试
} finally {
JDBCUtils.close(connection, preparedStatement, resultSet);
}
}
}
写法如下:
String sql = "select id,name,age,gender,salary,hiredate,tel from t_employee where name like ?";
preparedStatement.setString(1, "_O%");
注意不能写成下面酱紫:由于占位符 ? 被单引号包裹,因此这个占位符是无效的。
String sql = "select id,name,age,gender,salary,hiredate,tel from t_employee where name like '_?%'";
preparedStatement.setString(1, "O");
如果要向数据库的某张表中插入一万条数据,单纯使用循化一条一条的插入,非常耗时,这时候就需要批处理操作了。
在进行大数据量插入时,批处理为什么可以提高程序的执行效率?
注意:启用批处理需要在URL后面添加这个的参数:rewriteBatchedStatements=true
jdbc.url=jdbc:mysql://localhost:3306/jdbc?rewriteBatchedStatements=true
package xyz.foragain.sectiontwo;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class BatchJDBC {
public static void main(String[] args) {
String sql = "insert into t_employee (name,age,gender,salary,hiredate,tel) values (?,?,?,?,?,?)";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection();
preparedStatement = connection.prepareStatement(sql);
int count = 0;
for (int i = 1; i <= 10000; i++) {
preparedStatement.setString(1, "老" + i);
preparedStatement.setInt(2, i);
preparedStatement.setString(3, "男");
preparedStatement.setDouble(4, Math.abs(i - 5000));
preparedStatement.setDate(5, Date.valueOf("2025-01-12"));
preparedStatement.setString(6, "11111111111");
preparedStatement.addBatch();
}
count += preparedStatement.executeBatch().length;
System.out.println("插入了" + count + "条数据");
} catch (SQLException e) {
System.err.println("SQL执行异常: " + e.getMessage());
e.printStackTrace(); // 打印详细堆栈信息,便于调试
} finally {
JDBCUtils.close(connection, preparedStatement, null);
}
}
}
#创建存储过程:
create procedure mypro(in n int, out sum int)
begin
set sum := 0;
repeat
if n % 2 = 0 then
set sum := sum + n;
end if;
set n := n - 1;
until n <= 0
end repeat;
end;
//调用存储过程:
package xyz.foragain.sectiontwo;
import java.sql.*;
public class CallSPJDBC {
public static void main(String[] args) {
String sql = "{call mypro(?, ?)}";
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JDBCUtils.getConnection();
CallableStatement callableStatement = connection.prepareCall(sql);
callableStatement.setInt(1, 100);//给第一个?传值
callableStatement.registerOutParameter(2, Types.INTEGER);//设置第二个?的出参类型
callableStatement.execute();
int result = callableStatement.getInt(2);//获取第二个?的出参值
System.out.println("计算结果:" + result);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCUtils.close(connection, preparedStatement, null);
}
}
}
步骤:键盘读入=>编写SQL语句=>外部声明三引用=>JDBC编程六步走(数据库连接对象(第一步在工具类中)=>数据库操作对象(往SQL语句中传值)=>执行SQL语句=>处理结果集对象=>释放资源)
可以发现工具类(第一版)的引入减少了读取配置文件的步骤。此外使用preparedStatement引用时会发现多数代码都在编写往SQL语句中传值的操作,并且增删改查这些代码重复率高,思考如何解决?===>BaseDao类。
注意使用preparedStatement时,可以发现占位符?无需引号包裹
事务是数据库操作中不可分割的完整业务单元,需依赖多条 DML 语句协同完成。其核心价值在于通过原子性机制,确保多条 DML 语句要么全部成功提交、要么全部回滚失败,以此杜绝数据不一致问题,从根本上保障数据的安全性与完整性。
JDBC 事务默认处于自动提交模式,即每执行一条 DML 语句后数据库会立即持久化操作结果。这种机制存在显著风险:当业务包含多条 DML 语句时,若第一条执行成功后自动提交,而第二条因异常中断,会导致部分操作生效、部分操作失败,造成数据状态混乱。典型如银行账户转账场景:用户 A 扣款与用户 B 入账需两条 DML 语句配合,若自动提交下第一条执行后系统异常,会导致 A 账户资金减少但 B 未到账,严重破坏金融业务的一致性。
第一步:在获取数据库连接后,需立即将 JDBC 的自动提交模式关闭,切换为手动事务管理模式。此操作需放置在try
代码块的起始位置,以确保后续所有数据库操作均处于同一事务上下文内:
connection.setAutoCommit(false); // 禁用自动提交,开启手动事务管理
1、该设置仅对当前连接有效,不同 Connection 实例需单独配置
2、若未显式调用setAutoCommit(false),每条 SQL 语句将自动提交,无法回滚
3、建议在获取连接后立即设置,避免部分操作自动提交导致数据不一致
第二步:在所有业务逻辑涉及的 DML 语句(如 INSERT/UPDATE/DELETE)成功执行完毕后,需在try
代码块的末尾显式提交事务,使所有变更永久生效:
connection.commit(); // 所有业务操作成功后提交事务
1、提交操作需在所有关联 DML 语句执行完成后调用,确保业务完整性
2、一旦提交,事务即结束,无法再回滚此前操作
3、建议将提交操作作为try块的最后一步,确保异常不会跳过提交流程
第三步:在try
块执行过程中,若任何 DML 语句抛出异常(如 SQL 语法错误、约束冲突、网络中断等),需在catch
块中立即回滚事务,撤销所有已执行的操作:
connection.rollback(); // 撤销当前事务中的所有变更
1、回滚操作应优先于其他异常处理逻辑,确保第一时间撤销未完成事务
2、执行回滚前需验证connection不为空,避免 NPE 异常
3、需捕获rollback()本身可能抛出的异常(如连接已关闭)
4、回滚后事务立即结束,后续操作需重新开启事务
#数据库中表及数据的准备
DROP TABLE IF EXISTS t_fund;
CREATE TABLE t_fund (
id bigint AUTO_INCREMENT PRIMARY KEY,
actno varchar(255) NOT NULL UNIQUE COMMENT '账户编号',
balance decimal(19,4) NOT NULL DEFAULT 0.0000 COMMENT '账户余额'
);
START TRANSACTION;
INSERT INTO t_fund (actno, balance) VALUES
('actno1', 50000.0000),
('actno2', 0.0000);
COMMIT;
package xyz.foragain;
import xyz.foragain.sectiontwo.JDBCUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class FundTransferJDBC {
public static void main(String[] args) {//还可以添加判断账户是否存在以及账户金额是否充足功能
double transferMoney = 10000;
String sql1 = "update t_fund set balance = balance - ? where actno = ?";
String sql2 = "update t_fund set balance = balance + ? where actno = ?";
Connection connection = null;
PreparedStatement preparedStatement1 = null;
PreparedStatement preparedStatement2 = null;
try{
// 获取数据库连接并开启事务
connection = JDBCUtils.getConnection();
connection.setAutoCommit(false);
preparedStatement1 = connection.prepareStatement(sql1);
preparedStatement1.setDouble(1,transferMoney);
preparedStatement1.setString(2,"actno1");
preparedStatement1.executeUpdate();
preparedStatement2 = connection.prepareStatement(sql2);
preparedStatement2.setDouble(1,transferMoney);
preparedStatement2.setString(2,"actno2");
preparedStatement2.executeUpdate();
// 所有操作成功,提交事务
connection.commit();
System.out.println("转账成功: " + transferMoney + " 从actno1到actno2");
} catch (SQLException e) {
// 发生异常,回滚事务
if (connection != null) {
try {
connection.rollback();
System.out.println("事务已回滚");
} catch (SQLException ex) {
System.err.println("回滚失败: " + ex.getMessage());
ex.printStackTrace();
}
}
System.err.println("转账失败: " + e.getMessage());
e.printStackTrace();
}finally {
JDBCUtils.close(null,preparedStatement1,null);
JDBCUtils.close(connection,preparedStatement2,null);
}
}
}
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);//读未提交
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);//读提交
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);//可重复读
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);//串行化
在三层架构的设计体系中,事务控制机制通常被部署于业务逻辑层。具体来说,业务逻辑层需要通过调用Connection
对象实现事务管理,且必须保证该对象与数据访问层所使用的Connection
对象完全一致 —— 若两者出现不一致,将直接导致事务控制失效(即无法对同一事务进行统一管理)。
为解决这一问题,业界普遍采用ThreadLocal
技术实现Connection
对象的线程级绑定。该方案的核心逻辑在于:通过将Connection
对象与当前线程进行绑定,确保同一线程内的所有数据库操作共享同一事务上下文。这种方式相比跨层函数传参(虽可行但不推荐),既能避免参数传递的复杂性,又能从架构层面保证事务管理的一致性与可靠性。
不使用线程池时,由于创建Connection对象就是建立两个进程之间的通信,这是非常耗费资源的。可见:一次完整的数据库操作,大部分时间都耗费在连接对象的创建。不使用线程池后果如下:
提前创建N个连接对象并存储于集合中,形成连接池缓存机制。当用户发起请求时,可直接从连接池中获取连接对象,无需重新创建,大幅提升资源调用效率。此外,连接对象采用池化管理模式,仅允许从连接池获取:若当前无空闲连接,请求将进入等待状态,这种机制可有效控制连接对象的创建数量,避免资源过度消耗。
无论使用何种连接池产品,只需面向 javax.sql.DataSource
接口调用方法,即可统一接入连接池功能。这种标准化设计使得应用程序与具体实现解耦,提升了系统的可替换性和扩展性。 此外,若需自定义连接池,只需实现 DataSource
接口并按需实现其方法(如 getConnection()
),即可无缝集成到现有应用中。自定义连接池允许针对特定场景优化连接管理策略(如连接超时、最大并发数等),同时保持与标准数据源接口的兼容性。
三步走:
第一步:引入Druid的jar包,可以使用Maven管理
第二步:编写配置文件
# 必须配置(注意键名是规范)
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc
username=root
password=123456
# 可选但常用配置
initialSize=10
minIdle=10
maxActive=20
第三步:编写相关代码
package xyz.foragain.sectionthree;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;
public class DruidJDBC {
public static void main(String[] args) {
try (InputStream inputStream = DruidJDBC.class.getClassLoader().getResourceAsStream("jdbc.properties")) {
// 验证配置文件是否存在
if (inputStream == null) {
throw new IllegalArgumentException("未找到jdbc.properties配置文件");
}
// 加载配置文件
Properties properties = new Properties();
properties.load(inputStream);
// 创建数据源
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 从连接池获取连接(使用try-with-resources自动关闭)
try (Connection connection = dataSource.getConnection()) {
// 执行数据库操作
System.out.println("成功获取数据库连接: " + connection);
//执行CRUD的代码………………………………
}
} catch (Exception e) {
System.err.println("数据库连接失败: " + e.getMessage());
e.printStackTrace();
}
}
}
三步走:
第一步:引入HikariCP的jar包,可以使用Maven管理
第二步:编写配置文件
# 必须配置(注意键名是规范)
driverClassName=com.mysql.cj.jdbc.Driver
jdbcUrl=jdbc:mysql://localhost:3306/jdbc
username=root
password=123456
# 可选但常用配置
initialSize=10
minIdle=10
maxActive=20
第三步:编写相关代码
package xyz.foragain.sectionthree;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;
public class HikariCPJDBC {
public static void main(String[] args) {
try (InputStream inputStream = DruidJDBC.class.getClassLoader().getResourceAsStream("jdbc.properties")) {
// 验证配置文件是否存在
if (inputStream == null) {
throw new IllegalArgumentException("未找到jdbc.properties配置文件");
}
// 加载配置文件
Properties properties = new Properties();
properties.load(inputStream);
// 创建数据源,并从连接池获取连接(使用try-with-resources自动关闭)
try (HikariDataSource hikariDataSource = new HikariDataSource(new HikariConfig(properties));
Connection connection = hikariDataSource.getConnection()) {
// 执行数据库操作
System.out.println("成功获取数据库连接: " + connection);
//执行CRUD的代码………………………………
}
} catch (Exception e) {
System.err.println("数据库连接失败: " + e.getMessage());
e.printStackTrace();
}
}
}
既然我们使用到了连接池,那不妨使用Druid连接池编写一个工具类:
package xyz.foragain.sectionthree;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
public class JDBCUtils {
private static DataSource dataSource = null;
static {
try (InputStream inputStream = DruidJDBC.class.getClassLoader().getResourceAsStream("jdbc.properties")) {
if (inputStream == null) {
throw new IllegalArgumentException("未找到jdbc.properties配置文件");
}
Properties properties = new Properties();
properties.load(inputStream);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void close(Connection connection, Statement statement, ResultSet resultSet){
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
System.err.println("关闭ResultSet失败: " + e.getMessage());
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
System.err.println("关闭Statement失败: " + e.getMessage());
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
System.err.println("关闭Connection失败: " + e.getMessage());
}
}
}
}
注意事项:在第四章的实践中已明确 —— 若要确保事务完整性,需保证多次数据库操作使用同一Connection
连接对象。然而,上述工具类未能实现连接对象的一致性管理,因此需引入ThreadLocal
技术来解决该问题。
早在 JDK 1.2 版本,Java 便引入了 java.lang.ThreadLocal 类,为多线程编程中的并发问题开辟了一条全新路径。通过该工具类,开发者能够以简洁优雅的方式编写出高效的多线程应用程序。ThreadLocal 的典型应用场景包括管理多线程环境下的共享数据库连接、Session 会话等资源。
ThreadLocal 的核心作用是为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。在 Java 实现中,每个线程内部都维护着一个 ThreadLocalMap
ThreadLocal 的操作通过其提供的 set ()、get () 和 remove () 方法完成:对于同一个 static ThreadLocal 实例,不同线程在调用这些方法时,实际上操作的是各自线程内部的变量副本,线程间互不干扰。
package xyz.foragain.sectionfour;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import xyz.foragain.sectionthree.DruidJDBC;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
public class JDBCUtils {
private static DataSource dataSource = null;
private static final ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
// 静态初始化数据库连接池
static {
try (InputStream inputStream = DruidJDBC.class.getClassLoader().getResourceAsStream("jdbc.properties")) {
if (inputStream == null) {
throw new IllegalArgumentException("未找到jdbc.properties配置文件");
}
Properties properties = new Properties();
properties.load(inputStream);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (IOException e) {
throw new RuntimeException("加载数据库配置文件失败", e);
} catch (Exception e) {
throw new RuntimeException("初始化数据库连接池失败", e);
}
}
// 获取数据库连接
public static Connection getConnection() throws SQLException {
Connection connection = threadLocal.get();
if (connection == null || connection.isClosed()) {
connection = dataSource.getConnection();
threadLocal.set(connection);
}
return connection;
}
// 开启事务
public static void beginTransaction() throws SQLException {
Connection connection = getConnection();
connection.setAutoCommit(false);
}
// 提交事务
public static void commitTransaction() {
Connection connection = threadLocal.get();
if (connection != null) {
try {
connection.commit();
connection.setAutoCommit(true);
} catch (SQLException e) {
throw new RuntimeException("提交事务失败", e);
}
}
}
// 回滚事务
public static void rollbackTransaction() {
Connection connection = threadLocal.get();
if (connection != null) {
try {
connection.rollback();
connection.setAutoCommit(true);
} catch (SQLException e) {
throw new RuntimeException("回滚事务失败", e);
}
}
}
// 关闭资源 - 用于查询操作
public static void close(Connection connection, Statement statement, ResultSet resultSet) {
closeResultSet(resultSet);
closeStatement(statement);
closeConnection(connection);
}
// 关闭资源 - 用于更新操作
public static void close(Connection connection, Statement statement) {
closeStatement(statement);
closeConnection(connection);
}
private static void closeResultSet(ResultSet resultSet) {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
System.err.println("关闭ResultSet失败: " + e.getMessage());
}
}
}
private static void closeStatement(Statement statement) {
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
System.err.println("关闭Statement失败: " + e.getMessage());
}
}
}
// 改进的连接关闭逻辑
private static void closeConnection(Connection connection) {
try {
// 如果传入的连接与ThreadLocal中的连接相同且为自动提交模式,则关闭
if (connection != null && connection == threadLocal.get()) {
if (connection.getAutoCommit()) {
threadLocal.remove();
connection.close();
}
}
} catch (SQLException e) {
System.err.println("关闭数据库连接失败: " + e.getMessage());
}
}
// 强制关闭当前线程的连接(用于事务结束后)
public static void closeCurrentConnection() {
Connection connection = threadLocal.get();
try {
if (connection != null && !connection.isClosed()) {
// 确保事务已完成
if (!connection.getAutoCommit()) {
connection.setAutoCommit(true);
}
connection.close();
}
} catch (SQLException e) {
System.err.println("强制关闭数据库连接失败: " + e.getMessage());
} finally {
threadLocal.remove();
}
}
}
下面是t_employee表对应的一个实体pojo类:
package xyz.foragain.sectionfour;
import java.util.Date;
public class Employee {
private Long id;
private String name;
private Integer age;
private String gender;
private Double salary;
private Date hiredate;
private String tel;
//无参数和有参数构造方法
//getter和setter方法
//toString方法
}
DAO(Data Access Object,数据访问对象)是 JavaEE 体系中经典的设计模式之一,其核心功能是将对数据库增删改查(CRUD)的操作进行封装,形成独立的数据访问层。具体而言,该模式遵循 “一张数据库表对应一个 DAO 类” 的设计原则,通过将对底层数据库的操作(如 SQL 语句执行、连接管理等)抽象为类的方法,实现数据访问逻辑与业务逻辑的分离。
DAO 模式的核心设计目标:
UserDAO接口
与UserDAOImpl实现类
),便于后续扩展新的数据访问逻辑(如添加缓存策略、分库分表等)。在编写CRUD操作时,我们往往需要多次调用PreparedStatement的setter方法来为SQL语句中的占位符赋值,这一过程在第三章的示例中已经显得十分繁琐。为了简化这一流程,我们可以引入BaseDao类作为数据访问层的基类,将这些重复的参数设置逻辑封装在BaseDao中。之后,具体的DAO实现类只需继承BaseDao并实现对应的DAO接口,即可轻松复用这些通用的数据库操作逻辑,从而显著提高代码的可维护性和开发效率。
以下是一个BaseDao类:
package xyz.foragain.sectionfour;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class BaseDao {
public int executeUpdate(String sql, Object... params) {
Connection connection = null;
PreparedStatement statement = null;
try {
connection = JDBCUtils.getConnection();
statement = connection.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
statement.setObject(i + 1, params[i]);
}
return statement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException("SQL执行失败", e);
} finally {
JDBCUtils.close(connection, statement);
}
}
public <T> T executeQueryBean(Class<T> clazz, String sql, Object... params) {
List<T> list = executeQueryAll(clazz, sql, params);
return list.isEmpty() ? null : list.get(0);
}
public <T> List<T> executeQueryAll(Class<T> clazz, String sql, Object... params) {
List<T> result = new ArrayList<>();
Connection connectione = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connectione = JDBCUtils.getConnection();
statement = connectione.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
statement.setObject(i + 1, params[i]);
}
resultSet = statement.executeQuery();
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
while (resultSet.next()) {
Constructor<T> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
T instance = constructor.newInstance();
for (int i = 1; i <= columnCount; i++) {
Object value = resultSet.getObject(i);
String columnLabel = metaData.getColumnLabel(i);
Field field = clazz.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(instance, value);
}
result.add(instance);
}
} catch (SQLException | ReflectiveOperationException e) {
throw new RuntimeException("查询对象列表失败", e);
} finally {
JDBCUtils.close(connectione, statement, resultSet);
}
return result;
}
}
以下是t_employee表对应的EmployeeDao接口和EmployeeDaoImpl实现类:
package xyz.foragain.sectionfour;
import java.sql.SQLException;
import java.util.List;
public interface EmployeeDao {
//增
int insert(Employee employee) throws SQLException;
//删
int deleteById(Long id);
//改
int update(Employee employee);
//查询所有
List<Employee> selectAll();
//根据id查单个
Employee selectById(Long id);
}
package xyz.foragain.sectionfour;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;
public class EmployeeDaoImpl extends BaseDao implements EmployeeDao {
@Override
public int insert(Employee employee) throws SQLException {
String sql = "INSERT INTO employee (name, age, gender, salary, hiredate, tel) " +
"VALUES (?, ?, ?, ?, ?, ?)";
return executeUpdate(sql,
employee.getName(),
employee.getAge(),
employee.getGender(),
employee.getSalary(),
employee.getHiredate(),
employee.getTel());
}
@Override
public int deleteById(Long id) {
String sql = "DELETE FROM employee WHERE id = ?";
return executeUpdate(sql, id);
}
@Override
public int update(Employee employee) {
String sql = "UPDATE employee SET " +
"name = ?, age = ?, gender = ?, salary = ?, hiredate = ?, tel = ? " +
"WHERE id = ?";
return executeUpdate(sql,
employee.getName(),
employee.getAge(),
employee.getGender(),
employee.getSalary(),
employee.getHiredate(),
employee.getTel(),
employee.getId());
}
@Override
public List<Employee> selectAll() {
String sql = "SELECT * FROM employee";
return executeQueryAll(Employee.class, sql);
}
@Override
public Employee selectById(Long id) {
String sql = "SELECT * FROM employee WHERE id = ?";
return executeQueryBean(Employee.class, sql, id);
}
}