一篇搞定JDBC

第一章、JDBC的介绍

1.1 JDBC的含义

JDBC也即Java DataBase Connectivity的缩写,表示Java数据库连接:用Java语言向数据库发送SQL语句来操作数据库。

1.2 JDBC的原理

JDBC其实就是一组由SUN公司制定的规范(一组接口)。各个数据库厂商遵循该规范并编写相关的实现类(这里的实现类被称为驱动,各个数据库厂商提供的驱动不同),其他程序员只需将这些实现类导入自己的相关程序(如何导入请自己查询classpath的相关知识),并面向接口编程,即可访问数据库。

由此可以提炼出三个角色:

  • 其他程序员(对数据库中数据进行增删改查的Java程序员)
  • JDBC规范的制定者(SUN公司)
  • JDBC的实现者(各个数据库厂厂商)

JDBC规范所在的包(JDK里面):java.sql.

MySQL驱动的下载链接:MySQL :: Download Connector/J(可以通过Maven管理)

第二章、JDBC入门

2.1 准备工作:数据库表及数据的准备

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');

2.2 JDBC编程六步走

2.2.1 第一步、注册驱动

作用一:将 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机制自动完成注册。但实际开发中,部分数据库驱动不支持自动发现,仍需手动注册。因此建议保留手动注册步骤,以确保兼容性和代码健壮性。

2.2.2 第二步、获取数据库连接对象

#准备工作:为遵循 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()方法有重载

2.2.3 第三步、获取数据库操作对象

作用:获取java.sql.Statement对象,该对象负责将SQL语句发送给数据库,数据库负责执行该SQL语句
Statement statement = connection.createStatement();

2.2.4 第四步、执行SQL语句

作用:执行具体的SQL语句,例如:insert delete update select等。
String sqlSelect = "select id,name,age,gender,salary,hiredate,tel from t_employee";
resultSet = statement.executeQuery(sqlSelect);

2.2.5 第五步、处理查询结果集(select语句才有)

前提:如果之前的操作是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);
}

2.2.6 第六步释放资源

作用一:避免资源浪费:数据库连接属于稀缺资源,不释放会导致连接池耗尽或 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()

2.3 增删改查(忽略SQL注入)

2.3.1 增

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();
        }
    }
}

2.3.2 删

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();
        }
    }
}

2.3.3 改

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();
        }
    }
}

2.3.4 查

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();
        }
    }
}

2.3.5 获取新增行的主键

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();
        }
    }
}

2.3.6 获取元数据信息(也即列的信息)

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();
        }
    }
}

2.4 总结

**步骤:**键盘读入=>读取配置文件信息=>编写SQL语句=>JDBC编程六步走(注册驱动=>数据库连接对象=>数据库操作对象=>执行SQL语句=>处理结果集对象=>释放资源)

  • 增删改执行SQL语句的方法是executeUpdate,查则是executeQuery。当然也有其他方法,请自主查询
  • 增删改查的重复代码太多,可以提取出一个工具类供使用
  • 上述代码中的SQL语句使用了拼接操作,存在SQL注入风险
  • 增删改查一起使用容易产生事务问题

第三章、SQL注入

3.1 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注入。

以上操作明显违背了条件查询的初衷,设置出条件查询本来就是为了过滤出相关人员,如果不相关人员也可以随意查询,这明显是一个及其危险的操作。

3.2 SQL注入的避免

SQL注入是Statement接口造成的。Statement执行原理是:先进行字符串的拼接,再将拼接好的SQL语句发送给数据库服务器,数据库服务器进行SQL语句的编译,然后执行。因此用户提供的信息中如果含有SQL语句的关键字,那么这些关键字可以参加SQL语句的编译,导致原SQL语句被扭曲。(先拼接后编译

为了解决SQL注入,JDBC引入了一个新的接口PreparedStatement(预编译的数据库操作对象)。PreparedStatement是Statement接口的子接口。它俩是继承关系。PreparedStatement执行原理是:先对SQL语句进行预先的编译,然后再向SQL语句指定的位置传值,也就是说:用户提供的信息中即使含有SQL语句的关键字,那么这个信息也只会被当做一个值传递给SQL语句,用户提供的信息不再参与SQL语句的编译了,这样就解决了SQL注入问题。(先编译后拼接

  • 其他区别:
    • 从执行效率看,PreparedStatement 对同一条 SQL 语句仅需编译一次,后续多次执行时直接复用编译计划,尤其适用于高频操作场景(如批量插入),性能比 Statement 每次都重新编译 SQL 更优。
    • PreparedStatement 在绑定参数时会进行严格的类型检查(如setString()强制校验字符串类型),避免因参数类型不匹配导致的运行时错误,而 Statement 因采用字符串拼接,无法在编译阶段检测类型问题,存在潜在异常风险。

3.3 工具类JDBCUtils引入(第一版)

从第二章可以发现:增删改查的重复代码太多,因此可以提取出一个工具类供使用

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());
            }
        }
    }
}

3.4 增删改查

3.4.1 增

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);
        }
    }
}

3.4.2 删

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);
        }
    }
}

3.4.3 改

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);
        }
    }
}

3.4.4 查

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);
        }
    }
}

3.4.5 获取新增行的主键

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);
        }
    }
}

3.4.6 获取元数据信息(也即列的信息)

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);
        }
    }
}

3.4.7 模糊查询注意事项

写法如下:
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");

3.4.8 批处理操作

如果要向数据库的某张表中插入一万条数据,单纯使用循化一条一条的插入,非常耗时,这时候就需要批处理操作了。

在进行大数据量插入时,批处理为什么可以提高程序的执行效率?

  1. 减少了网络通信次数:JDBC 批处理会将多个 SQL 语句一次性发送给服务器,减少了客户端和服务器之间的通信次数,从而提高了数据写入的速度,特别是对于远程服务器而言,优化效果更为显著。
  2. 减少了数据库操作次数:JDBC 批处理会将多个 SQL 语句合并成一条 SQL 语句进行执行,从而减少了数据库操作的次数,减轻了数据库的负担,大大提高了数据写入的速度。

注意:启用批处理需要在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);
        }
    }
}

3.4.9 调用存储过程

#创建存储过程:
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);
        }
    }
}

3.5 总结

步骤:键盘读入=>编写SQL语句=>外部声明三引用=>JDBC编程六步走(数据库连接对象(第一步在工具类中)=>数据库操作对象(往SQL语句中传值)=>执行SQL语句=>处理结果集对象=>释放资源)

可以发现工具类(第一版)的引入减少了读取配置文件的步骤。此外使用preparedStatement引用时会发现多数代码都在编写往SQL语句中传值的操作,并且增删改查这些代码重复率高,思考如何解决?===>BaseDao类。

注意使用preparedStatement时,可以发现占位符?无需引号包裹

  • statement和preparedStatement区别:
    • 同:
      • statement和preparedStatement都是由connection创建的
      • 执行SQL语句都是由statement或preparedStatement调用executeUpdate或executeQuery方法执行
    • 异:
      • SQL的传入位置发生改变:
        • 在statement中是statement调用executeUpdate或executeQuery方法传入
        • 在preparedStatement中是connection创建preparedStatement对象时传入
      • SQL语句的写法不同:
        • 在statement中是通过SQL字符串拼接实现
        • 在preparedStatement中是通过?占位,之后通过preparedStatement的setString方法传入,注意不单单setString方法可以传值,还有其他方法(setInt,setBlob等),请自行查看

第四章、事务

事务是数据库操作中不可分割的完整业务单元,需依赖多条 DML 语句协同完成。其核心价值在于通过原子性机制,确保多条 DML 语句要么全部成功提交、要么全部回滚失败,以此杜绝数据不一致问题,从根本上保障数据的安全性与完整性。

JDBC 事务默认处于自动提交模式,即每执行一条 DML 语句后数据库会立即持久化操作结果。这种机制存在显著风险:当业务包含多条 DML 语句时,若第一条执行成功后自动提交,而第二条因异常中断,会导致部分操作生效、部分操作失败,造成数据状态混乱。典型如银行账户转账场景:用户 A 扣款与用户 B 入账需两条 DML 语句配合,若自动提交下第一条执行后系统异常,会导致 A 账户资金减少但 B 未到账,严重破坏金融业务的一致性。

4.1 添加事务

第一步:在获取数据库连接后,需立即将 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、回滚后事务立即结束,后续操作需重新开启事务

4.2 实例

#数据库中表及数据的准备
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);
        }

    }
}

4.3 设置JDBC事务隔离级别

connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);//读未提交
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);//读提交
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);//可重复读
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);//串行化

4.4 总结

三层架构的设计体系中,事务控制机制通常被部署于业务逻辑层。具体来说,业务逻辑层需要通过调用Connection对象实现事务管理,且必须保证该对象与数据访问层所使用的Connection对象完全一致 —— 若两者出现不一致,将直接导致事务控制失效(即无法对同一事务进行统一管理)。

为解决这一问题,业界普遍采用ThreadLocal技术实现Connection对象的线程级绑定。该方案的核心逻辑在于:通过将Connection对象与当前线程进行绑定,确保同一线程内的所有数据库操作共享同一事务上下文。这种方式相比跨层函数传参(虽可行但不推荐),既能避免参数传递的复杂性,又能从架构层面保证事务管理的一致性与可靠性。

第五章、连接池

5.1 连接池的介绍

5.1.1 不使用线程池带来的缺点

不使用线程池时,由于创建Connection对象就是建立两个进程之间的通信,这是非常耗费资源的。可见:一次完整的数据库操作,大部分时间都耗费在连接对象的创建。不使用线程池后果如下:

  • 每一次请求都创建一个Connection连接对象,效率较低。
  • 连接对象的数量无法限制。如果连接对象的数量过高,会导致mysql数据库服务器崩溃。

5.1.2 使用线程池的优势

提前创建N个连接对象并存储于集合中,形成连接池缓存机制。当用户发起请求时,可直接从连接池中获取连接对象,无需重新创建,大幅提升资源调用效率。此外,连接对象采用池化管理模式,仅允许从连接池获取:若当前无空闲连接,请求将进入等待状态,这种机制可有效控制连接对象的创建数量,避免资源过度消耗。

5.1.3 javax.sql.DataSource

无论使用何种连接池产品,只需面向 javax.sql.DataSource 接口调用方法,即可统一接入连接池功能。这种标准化设计使得应用程序与具体实现解耦,提升了系统的可替换性和扩展性。 此外,若需自定义连接池,只需实现 DataSource 接口并按需实现其方法(如 getConnection()),即可无缝集成到现有应用中。自定义连接池允许针对特定场景优化连接管理策略(如连接超时、最大并发数等),同时保持与标准数据源接口的兼容性。

5.1.4 线程池的基本属性

  1. **初始化连接数(initialSize):**连接池启动时预先创建的连接数量。合理设置可避免首次请求时的连接创建延迟,通常建议与 minIdle 保持一致。
  2. **最大连接数(maxActive/maxTotal):**连接池允许创建的最大连接数量。当达到此阈值时,新请求将被阻塞,直到有连接被释放。需根据数据库性能、应用并发量及系统资源综合评估,避免过高导致数据库资源耗尽或过低造成请求堆积。
  3. **最小空闲连接数(minIdle):**连接池保持的最小空闲连接数量。即使无请求时,连接池也会维持此数量的连接,以应对突发流量。建议设置为系统平均并发连接数,减少频繁创建 / 销毁连接的开销。
  4. **最大空闲连接数(maxIdle):**连接池允许保留的最大空闲连接数量。超过此值的空闲连接将被释放,以避免资源浪费。通常与 maxActive 保持一致,防止频繁释放导致的性能波动。
  5. **最大等待时间(maxWaitMillis):**当连接池满时,新请求的最长等待时间(毫秒)。超时将抛出SQLException,需结合业务容忍度设置,建议值为30000ms(30 秒)。
  6. 连接有效检查:
    1. **testOnBorrow:**获取连接时检查有效性,确保返回的连接可用,但会增加请求延迟。
    2. **testOnReturn:**归还连接时检查有效性,通常设为 false 以提升性能。
    3. **替代方案:**结合 testWhileIdle 和 timeBetweenEvictionRunsMillis(定期检查空闲连接),平衡性能与可靠性。
  7. 数据库连接参数:
    1. **driverClassName:**数据库驱动类(如com.mysql.cj.jdbc.Driver)。
    2. **url:**数据库连接 URL,需包含参数(如 useSSL=false、serverTimezone=UTC)。
    3. **username/password:**数据库凭证,建议加密存储或通过配置中心管理。

5.1.5 常用的连接池

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

5.2 Druid连接池的使用

三步走:

第一步:引入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();
        }
    }
}

5.3 HikariCP连接池的使用

三步走:

第一步:引入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();
        }
    }
}

5.4 工具类JDBCUtils引入(第二版)

既然我们使用到了连接池,那不妨使用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技术来解决该问题。

5.5 ThreadLocal

早在 JDK 1.2 版本,Java 便引入了 java.lang.ThreadLocal 类,为多线程编程中的并发问题开辟了一条全新路径。通过该工具类,开发者能够以简洁优雅的方式编写出高效的多线程应用程序。ThreadLocal 的典型应用场景包括管理多线程环境下的共享数据库连接、Session 会话等资源。

ThreadLocal 的核心作用是为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。在 Java 实现中,每个线程内部都维护着一个 ThreadLocalMap 实例,其中键为 ThreadLocal 对象本身,值则是该线程对应的共享变量。

ThreadLocal 的操作通过其提供的 set ()、get () 和 remove () 方法完成:对于同一个 static ThreadLocal 实例,不同线程在调用这些方法时,实际上操作的是各自线程内部的变量副本,线程间互不干扰。

  • 优势:
    • 跨层数据传递:在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
    • 线程数据隔离:各线程间的数据相互独立,避免同步开销
    • 事务上下文管理:在事务操作中存储线程专属的事务状态
    • 资源管理:典型场景如数据库连接、Session 会话等线程独享资源的管理
  • 常用方法:
    • ThreadLocal.get():获取当前线程的变量副本
    • ThreadLocal.set(T value):设置当前线程的变量副本
    • ThreadLocal.remove():移除当前线程的变量副本

5.6 工具类JDBCUtils引入(第三版)

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();
        }
    }
}

第六章、Dao的介绍

6.1 实体类与ORM的引入

  • 在使用JDBC操作数据库时,我们会发现数据都是零散的,明明在数据库中是一行完整的数据,到了Java中变成了一个一个的变量,不利于维护和管理。而我们Java是面向对象的,一个表对应的是一个类,一行数据就对应的是Java中的一个对象,一个列对应的是对象的属性,所以我们要把数据存储在一个载体里,这个载体就是实体类!
  • ORM(Object Relational Mapping)思想,对象到关系数据库的映射,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,以面向对象的角度操作数据库中的数据,即一张表对应一个类,一行数据对应一个对象,一个列对应一个属性!
  • 当下JDBC中这种过程我们称其为手动ORM。后续我们也会学习ORM框架,比如MyBatis、JPA等。

下面是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方法

}

6.2 Dao的引入

DAO(Data Access Object,数据访问对象)是 JavaEE 体系中经典的设计模式之一,其核心功能是将对数据库增删改查(CRUD)的操作进行封装,形成独立的数据访问层。具体而言,该模式遵循 “一张数据库表对应一个 DAO 类” 的设计原则,通过将对底层数据库的操作(如 SQL 语句执行、连接管理等)抽象为类的方法,实现数据访问逻辑与业务逻辑的分离。

DAO 模式的核心设计目标:

  1. 提升代码复用性
    将通用的数据操作(如查询、更新)封装为可复用的方法,避免在业务逻辑中重复编写数据库操作代码,减少冗余。
  2. 降低系统耦合度
    业务层仅需调用 DAO 接口的方法,无需关心底层数据库连接、SQL 执行等细节,若数据库类型或表结构变更,只需修改 DAO 实现类,无需调整业务代码。
  3. 增强系统扩展性
    通过接口与实现分离的设计(如定义UserDAO接口UserDAOImpl实现类),便于后续扩展新的数据访问逻辑(如添加缓存策略、分库分表等)。

6.3 BaseDao的引入

在编写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);
    }

}

你可能感兴趣的:(数据库,mysql,java)