第9章 数据库编程

我们在开发Java程序时,数据都是存储在内存中,属于临时存储,当程序停止或重启时,内存中的数据就丢失了!我们为了解决数据的长期存储问题,有如下解决方案:

  1. 数据通过I/O流技术,存储在本地磁盘中,解决了持久化问题,但是没有结构和逻辑,不方便管理和维护。
  2. 通过关系型数据库,将数据按照特定的格式交由数据库管理系统维护。关系型数据库是通过库和表分隔不同的数据,表中数据存储的方式是行和列,区分相同格式不同值的数据。

在这里插入图片描述

数据存储在数据库,仅仅解决了我们数据存储的问题,但当我们程序运行时,需要读取数据,以及对数据做增删改的操作,那么我们如何通过Java程序对数据库中的数据做增删改查呢?

在这里插入图片描述

9.0 课程准备

MySQL的安装和MySQL服务运行

9.1 JDBC

9.1.1 JDBC概述

在JDBC(Java Database Connectivity,Java数据库互联)出现之前,各数据库厂商提供自己独立的数据库驱动程序,开发人员想要操作不同类型的数据库,需要编写专门针对每个数据库的程序。

例如,要访问MySQL数据库就需要编写一种程序,要访问Oracle数据库就需要编写另一种程序。这种情况下,应用程序的可移植性非常差,因为每次切换数据库都需要修改和适配程序。

JDBC的出现解决了上述问题,JDBC是一套访问数据库的标准Java类库,它定义了应用程序访问和操作数据库的API。通过JDBC,开发人员可以使用相同的API操作MySQL、Oracle或其他关系型数据库。

在这里插入图片描述

JDBC核心组成

  • 接口规范:
    • 为了项目代码的可移植性,可维护性,SUN公司从最初就制定了Java程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种DBMS软件,Java代码可以保持一致性。
    • 接口存储在java.sql和javax.sql包下。
  • 实现规范:
    • 因为各个数据库厂商的DBMS软件各有不同,那么各自的内部如何通过SQL实现增、删、改、查等操作管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。
    • 厂商将实现内容和过程封装成jar文件,我们程序员只需要将jar文件引入到项目中集成即可,就可以开发调用实现过程操作数据库了。

9.1.2 JDBC快速入门

1)JDBC搭建步骤
  1. 准备数据库。
  2. 创建项目,引入依赖
  3. 编写代码
2)数据库
CREATE DATABASE IF NOT EXISTS jdbc_demo CHARACTER SET utf8mb4;

USE jdbc_demo;

CREATE TABLE t_space(
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(40),
	type VARCHAR(40),
	launchTime DATE		
);
INSERT INTO t_space (name, type, launchTime) VALUES
	('嫦娥一号', '探月卫星', '2007-10-24'),
	('神舟五号', '载人飞船', '2003-10-15'),
	('天宫一号', '空间实验室', '2011-09-29'),
	('北斗一号', '导航卫星', '2000-10-31'),
	('天问一号', '火星探测器', '2020-07-23'),
	('神舟十二号', '载人飞船', '2021-06-17'),
	('嫦娥三号', '月球着陆器', '2013-12-02'),
	('天和核心舱', '空间站舱段', '2021-04-29'),
	('风云一号', '气象卫星', '1988-09-07'),
	('实践一号', '科学实验卫星', '1971-03-03'),
	('神舟七号', '载人飞船', '2008-09-25'),
	('嫦娥五号', '月球采样探测器', '2020-11-24'),
	('祝融号', '火星巡视车', '2021-05-15'),
	('梦天实验舱', '空间站实验舱', '2022-10-31'),
	('问天实验舱', '空间站实验舱', '2022-07-24'),
	('高分一号', '遥感卫星', '2013-04-26'),
	('羲和号', '太阳探测卫星', '2021-10-14'),
	('神舟十六号', '载人飞船', '2023-05-30'),
	('鹊桥号', '中继通信卫星', '2018-05-21'),
	('海洋一号', '海洋观测卫星', '2002-05-15');
3)引入依赖
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>8.0.31</version>
</dependency>

<dependency>
   <groupId>com.mysql</groupId>
   <artifactId>mysql-connector-j</artifactId>
   <version>8.0.33</version>
</dependency>
4)Java代码
package dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class TestJDBC {
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) throws Exception {
        // 1、注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");

        // 2、获取数据库连接
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);

        // 3、创建Statement对象
        Statement stmt = conn.createStatement();

        // 4、编写SQL语句并执行,获取结果
        String sql = "select * from t_space";
        ResultSet rs = stmt.executeQuery(sql);

        // 5、处理结果
        while (rs.next()) {
            int id = rs.getInt("id");
            String name = rs.getString("name");
            String type = rs.getString("type");
            String launchTime = rs.getString("launchTime");
            System.out.println(id + "\t" + name + "\t" + type + "\t" + launchTime);
        }

        // 6、释放资源(先开后关原则)
        rs.close();
        stmt.close();
        conn.close();

    }
}
5)总结
  1. 注册驱动【依赖的驱动类,进行安装】
  2. 获取连接【Connection建立连接】
  3. 创建发送SQL语句对象【Connection创建发送SQL语句的Statement】
  4. 发送SQL语句,并获取返回结果【Statement 发送sql语句到数据库并且取得返回结果】
  5. 结果集解析【结果集解析,将查询结果解析出来】
  6. 资源关闭【释放ResultSet、Statement 、Connection】

9.1.3 核心API

1)注册驱动(Driver)
Class.forName("com.mysql.cj.jdbc.Driver");
  • 在 Java 中,当使用 JDBC(Java Database Connectivity)连接数据库时,需要加载数据库特定的驱动程序,以便与数据库进行通信。加载驱动程序的目的是为了注册驱动程序,使得 JDBC API 能够识别并与特定的数据库进行交互。

  • 注:从JDK6开始,不再需要显式地调用 Class.forName() 来加载 JDBC 驱动程序,只要在类路径中集成了对应的jar文件,会自动在初始化时注册驱动程序。(SPI机制)(META-INF/services/java.sql.Driver文件)

2)DriverManager

1、注册驱动

DriverManager类是用于管理JDBC驱动程序的类,该类中定义了注册驱动程序及获取数据库连接对象的静态方法。常用方法:

  • static void registerDriver(Driver driver):用于在DriverManager中注册给定的JDBC驱动程序
  • static Connection getConnection (String url,String user,String password) :用于建立和数据库的连接,并返回表示连接的Connection对象

虽然DriverManager类提供的registerDriver()方法可以用于注册驱动程序,但是编写代码时,一般不会使用这个方法注册驱动程序。因为Driver接口的实现类中通常有一个静态代码块,该静态代码块内部会执行DriverManager对象的registerDriver()方法注册驱动程序。所以如果使用registerDriver()方法注册驱动程序,相当于注册了两次驱动程序。

MySQL实现的Driver类源码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

2、获取连接

JDBC根据URL的信息与数据库建立连接,其URL需要按照特定的语法格式来配置。具体的URL格式会根据不同的数据库类型和驱动程序而有所变化,其中连接到MySQL数据库的URL语法格式如下。

jdbc:mysql://[hostname]:[port]/database-name[?参数键值对1&参数键值对2…]
jdbc:mysql://127.0.0.1:3306/jdbc_demo

其中:

  • “jdbc:mysql://”:表示JDBC 驱动程序使用 MySQL协议进行连接。
  • hostname:数据库的主机名或IP地址。
  • port:数据库的端口号,如果连接的是本机MySQL数据库,并且使用MySQL的默认端口号3306,则URL中的[hostname]:[port]可以省略。
  • database-name:数据库名称
  • 如果需要在URL中定制连接的一些属性和配置,可以在数据库名称后面使用问号(?)进行声明,如果需要添加多个参数键值对,参数键值对之间使用&进行连接。
    • 配置 useSSL=false参数,禁用安全连接方式,解决警告提示。
3)Connection

Connection接口用于表示与数据库的连接,是进行数据库操作的主要入口点。可以通过Connection对象与数据库建立连接,并进行各种数据库操作,包括执行SQL查询更新数据进行事务管理等。

常用方法:

方法 描述
Statement createStatement() 用于创建一个Statement对象,Statement对象可以执行静态SQL语句
PreparedStatement prepareStatement(String sql) 用于创建一个PreparedStatement对象,PreparedStatement对象可以执行预编译的SQL语句
void commit() 用于提交事务,使所有上一次提交/回滚后进行的更改成为持久更改,并释放当前Connection对象持有的所有数据库锁
void setAutoCommit(boolean autoCommit) 用于设置是否关闭自动提交模式
void roolback() 用于回滚事务,用于取消在当前事务中进行的所有更改,并释放当前Connection对象持有的所有数据库锁
void close() 用于关闭与数据库的连接并释放资源

事务管理

事务的概念:事务(Transaction)指一个操作由多个步骤组成,要么全部成功,要么全部失败。

案例:存在账户表t_account,建表语句如下:

CREATE TABLE `t_account` (
  `id` int NOT NULL,
  `balance` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

账户表中有以下两个账户:

INSERT INTO `jdbc_demo`.`t_account` (`id`, `balance`) VALUES (1, 500);
INSERT INTO `jdbc_demo`.`t_account` (`id`, `balance`) VALUES (2, 500);

现在id为1的账户要给id为2的账户转账500,相关JDBC代码如下:

public class JDBCDemo2ConnNoTrans {
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) throws Exception {

        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        Statement stmt = conn.createStatement();

        // 不开启事务,自动提交模式(默认也是这个模式)
        conn.setAutoCommit(true);

        String sql1 = "update t_account set balance=0 where id=1";
        int count1 = stmt.executeUpdate(sql1);
        System.out.println("第一条SQL影响的行数:" + count1);

        String sql2 = "update t_account set balance=balance+500 where id=2";
        int count2 = stmt.executeUpdate(sql2);
        System.out.println("第二条SQL影响的行数:" + count2);
        stmt.close();
        conn.close();
    }
}

问题:假设在16行代码和18行代码之间,出现了系统bug,那么代码运行后,数据库的数据被会正确修改吗?

比如:在16行代码和18行代码之间增加必定报错的异常代码

// 模拟异常
int i = 1 / 0; // 抛出 ArithmeticException

需求:如何让数据修改达成一致性,即如何让多条更新语句要么都成功,那么都失败?

public class JDBCDemo3ConnTrans {
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            stmt = conn.createStatement();

            // 开启事务
            conn.setAutoCommit(false);

            String sql1 = "update t_account set balance=0 where id=1";
            int count1 = stmt.executeUpdate(sql1);
            System.out.println("第一条SQL影响的行数:" + count1);

            // 模拟异常
            int i = 1 / 0; // 抛出 ArithmeticException

            String sql2 = "update t_account set balance=balance+500 where id=2";
            int count2 = stmt.executeUpdate(sql2);
            System.out.println("第二条SQL影响的行数:" + count2);

            // 提交事务
            conn.commit();

        } catch (Exception e) {
            e.printStackTrace();
            // 发生异常时回滚事务
            if (conn != null) {
                try {
                    conn.rollback();// 回滚事务
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        } finally {
            try {
                if (stmt != null) {
                    stmt.close();
                }
                if (conn != null) {
                    conn.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
4)Statement

Statement接口是Java中用于执行SQL语句的重要接口之一,该接口的实例可以通过Connection对象的createStatement()方法获取,通过该接口实例可以将静态SQL语句发送到数据库中进行编译与执行,并返回数据库的处理结果。

静态SQL语句是指在编译时就确定好的SQL语句,其结构和内容在程序运行期间不会发生变化。

Statement接口提供了三个常用方法用于执行SQL语句:

方法 描述
boolean execute(String sql) 用于执行SQL语句,该方法执行SQL语句后可能会返回多个结果,如果执行后结果为ResultSet对象,则返回true;如果执行后结果为受影响的行数或没有任何结果,则返回false
int executeUpdate(String sql) 用于执行SQL中的INSERT、UPDATE和DELETE语句。该方法返回一个int类型的值,表示数据库中受SQL语句影响的行数
ResultSet executeQuery(String sql) 用于执行SQL中的SELECT语句,该方法返回一个表示查询结果的ResultSet对象
5)ResultSet

ResultSet接口是JDBC中用于表示数据库查询结果集的接口,执行查询语句得到的结果集会被封装到一个ResultSet对象中。可以将ResultSet对象看作一个包含多个数据行的表格,每一行代表结果集中的一条记录,每一列代表每个字段的值。

ResultSet接口内部维护着一个游标,用于指示当前行的位置。初始化ResultSet对象时,游标位于第一行之前的位置。通过调用next()方法,可以将游标移动到下一行。
在这里插入图片描述

ResultSet接口的常用方法:

  • 获取查询结果:

    方法 描述
    boolean next() 1)将游标从结果集的当前位置移动到下一条记录
    2)判断当前记录是否为有效记录行
    xxx getXxx(int columnIndex) 通过字段的索引获取xxx类型的值,参数columnIndex代表字段在查询结果中的索引,注意:从1开始
    xxx getXxx(String columnName) 通过字段的名称获取xxx类型的值,参数columnName代表字段的名称

    在这里插入图片描述

    public class JDBCDemo5ResultSet {
        private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo";
        private static final String USERNAME = "root";
        private static final String PASSWORD = "123456";
    
    
        public static void main(String[] args) throws Exception {
    
            // 1、获取数据库连接
            Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            // 2、创建Statement对象
            Statement stmt = conn.createStatement();
            // 3、定义并执行SQL语句
            String sql = "select * from t_space";
            ResultSet rs = stmt.executeQuery(sql);
            // 4、处理结果
            while (rs.next()) {
                int id = rs.getInt(1);
                String name = rs.getString(2);
                String type = rs.getString(3);
                Date launchTime = rs.getDate(4);
                System.out.println(id + " " + name + " " + type + " " + launchTime);
            }
    
            // 5、释放资源
            rs.close();
            stmt.close();
            conn.close();
        }
    }
    
  • 其它方法

    方法 描述
    boolean absolute(int row) 将游标移动到结果集的第row条记录
    boolean previous() 将游标从结果集的当前位置移动到上一条记录
    void beforeFirst() 将游标移动到结果集开头(第一条记录之前)
    void afterLast() 将游标指针移动到结果集末尾(最后一条记录之后)
    boolean first() 将游标移动到结果集的第一条记录
    boolean last() 将游标移动到结果集的最后一条记录
    int getRow() 返回当前记录的行号
    Statement getStatement() 返回生成结果集的Statement对象
    void close() 释放此ResultSet对象的数据库和JDBC资源

案例需求:查询t_space表的数据,封装到Space对象中,并且存储到ArrayList集合中。

@Test
public void demo2() throws Exception {

    List<Space> list = new ArrayList<>();

    // 获取数据库连接
    Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
    // 创建Statement对象
    Statement stmt = conn.createStatement();
    // 定义并执行SQL语句
    String sql = "select * from t_space";
    ResultSet rs = stmt.executeQuery(sql);
    // 处理结果
    while (rs.next()) {
        int id = rs.getInt(1);
        String name = rs.getString(2);
        String type = rs.getString(3);
        Date launchTime = rs.getDate(4);
        Space space = new Space();
        space.setId(id);
        space.setName(name);
        space.setType(type);
        space.setLaunchTime(launchTime);
        list.add(space);
    }
    System.out.println(list);

    // 释放资源
    rs.close();
    stmt.close();
    conn.close();
}
6)PreparedStatement

JDBC API中的Statement接口可以用于执行SQL语句,从而实现对数据库的操作。然而,当使用 Statement接口执行用户输入的SQL语句时,如果不对输入的SQL语句进行把控,就可能受到SQL注入攻击攻击者可以通过恶意构造的输入来改变SQL语句的逻辑结构,导致不安全的操作发生。

在这里插入图片描述

正常SQL:

SELECT * FROM t_user WHERE username = 'john' AND password = 'mypass123'

SQL注入

# 用户名: 任意值
# 密码: 任意值' OR '1'='1

构造出的SQL:

SELECT * FROM t_user WHERE username = 'admin' AND password = '任意值' OR '1'='1' 

由于 ‘1’=‘1’ 永远为真,攻击者可以绕过认证

SQL注入案例:

  • 数据库

    CREATE TABLE `t_user` (
      `username` varchar(255) DEFAULT NULL,
      `password` varchar(255) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    
  • Java代码

    @Test
    public void demo1() throws Exception {
        // 获取数据库连接
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        // 创建Statement对象
        Statement stmt = conn.createStatement();
        // 定义并执行SQL语句
        String loginUsername = "fdghdh";
        String loginPassword = "asfasa' OR '1'='1";
        String sql = "select * from t_user where username = '" + loginUsername + "' and password ='" + loginPassword + "'";
        ResultSet rs = stmt.executeQuery(sql);
        // 处理结果
        while (rs.next()) {
            String username = rs.getString("username");
            String password = rs.getString("password");
            System.out.println(username + "::" + password);
        }
        // 释放资源
        rs.close();
        stmt.close();
        conn.close();
    }
    

为此,JDBC API提供了扩展的PreparedStatement接口,该接口的对象可以执行预编译的SQL语句。

预编译的SQL语句支持在SQL语句中使用占位符来代替具体的参数值,在每次执行时只需传递参数,而无须重新解析和编译SQL语句,提高了代码的可读性和数据库操作的灵活性。

PreparedStatement使用:

  1. 获取PreparedStatement对象

    // SQL语句中的参数,使用“?”占位符替代
    String sql = "select * from t_user where username = ? and password = ?";
    
    // 通过Connection对象获取PreparedStatement对象,并传入对象的SQL语句
    PreparedStatement pstmt = conn.prepareStatement(sql);
    
  2. 设置参数值

    # PreparedStatement对象: setXxx(参数1, 参数2): 给 ? 赋值
    - Xxx:数据类型;如setInt(参数1, 参数2),setString(参数1, 参数2)
      - 参数1:?的位置编号,从1开始
      - 参数2:?的值
    
  3. 执行SQL

    pstmt.executeQuery();//不需要再传递SQL语句
    pstmt.executeUpdate();
    
  4. 完整代码

    @Test
    public void demo2() throws Exception {
        // 获取数据库连接
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        // 定义SQL
        String sql = "select * from t_user where username = ?  and password = ?";
        String loginUsername = "zhangsan";
        String loginPassword = "asfasa' OR '1'='1";
    
        // 获取PreparedStatement对象
        PreparedStatement pstmt = conn.prepareStatement(sql);
    
        // 设置?的值
        pstmt.setString(1, loginUsername);
        pstmt.setString(2, loginPassword);
      
      	// 执行SQL
        ResultSet rs = pstmt.executeQuery();
      
      	// 处理结果
        while (rs.next()) {
            String username = rs.getString("username");
            String password = rs.getString("password");
            System.out.println(username + "::" + password);
        }
        // 释放资源
        rs.close();
        pstmt.close();
        conn.close();
    }
    

PreparedStatement原理(课后了解)

SQL注入

SELECT * FROM t_user WHERE username = 'zhangsan' AND password = 'asfasa' OR '1'='1' ;

PreparedStatement防止SQL注入:将敏感字符进行转义

asfasa' OR '1'='1 -> asfasa\' OR \'1\'=\'1
SELECT * FROM t_user WHERE username = 'zhangsan' AND password = 'asfasa\' OR \'1\'=\'1' ;

预编译(课后了解)

在这里插入图片描述

  1. PreparedStatement预编译功能开启:useServerPrepStmts=true

  2. 配置MySQL执行日志(重启MySQL服务生效)

    [mysqld]
    log-output=FILE
    general-log=1
    general_log_file="D:\mysql.log"
    

配置文件一般在安装目录或数据目录,具体请自行百度

原理:

  1. 在获取PreparedStatement对象时,将sql语句发送给mysql服务器进行检查,编译(这些步骤很耗时)
  2. 执行时就不用再进行这些步骤了,速度更快
  3. 如果sql模板一样,则只需要进行一次检查、编译

9.2 数据库连接池

9.2.1 引言

在JDBC编程中,默认情况下每次连接数据库都会创建一个Connection对象,使用完毕就会将其销毁。每一个数据库连接对象都对应一个物理的数据库连接,如果有大量并发访问时,频繁地创建、销毁数据库连接对象会影响数据库的访问效率,甚至导致数据库崩溃。

为了解决上述这种问题,数据库连接池应运而生。数据库连接池是个容器,负责分配、管理数据库连接对象,通过数据库连接池,程序可以复用连接对象,而不是每次连接数据库都重新创建和销毁数据库连接对象,可以大大提高系统的性能和效率。

在这里插入图片描述

为了更加方便地使用和管理数据库连接池,Java提供了DataSource接口,该接口为应用程序提供了标准化和可重用的方式管理和提供数据库连接。

DataSource 接口提供了两个获取数据库连接对象的方法:

//方法一:
Connection getConnection() 
//方法二:
Connection getConnection(String username, String password)

常见的数据库连接池:

  • DBCP

  • C3P0

  • Druid

    Druid是阿里巴巴公司旗下开源的数据库连接池项目,其功能强大,性能也比较优秀

9.2.2 Druid

Druid数据库连接池的使用:

  1. 引入依赖(导入jar包)
  2. 创建配置文件
  3. 加载配置文件
  4. 获取数据库连接池对象
  5. 获取连接
1)引入依赖
<!--Druid的依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.16</version>
</dependency>
2)创建配置文件
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc_demo?useSSL=true&ServerTimezone=UTC
username=root
password=123456
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间(单位:毫秒)
maxWait=3000
3,4,5)获取连接
public class JDBCDemo7Druid {
    public static void main(String[] args) throws Exception {
        // 加载druid配置文件
        Properties properties = new Properties();
        FileInputStream fis = new FileInputStream("src/main/resources/druid.properties");
        properties.load(fis);
        // 创建数据库连接池
        DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
        // 获取连接
        Connection conn = dataSource.getConnection();
        System.out.println(conn);
    }
}

获取到数据库连接对象后,就可以进行增删改查操作

9.3 Commons DbUtils

DbUtils – JDBC Utility Component

Commons DbUtils是一个开源的JDBC工具类库。它对JDBC进行了简单的封装,简化了JDBC进行数据查询和记录读取操作。

9.3.1 QueryRunner类

QueryRunner类提供了对SQL语句操作的API,它封装的查询、插入、更新和删除等数据库操作方法,可以更方便地进行数据库操作。

  • QueryRunner():用于创建一个与数据库无关的QueryRunner对象,后期再操作数据库时,需要手动提供一个Connection对象
  • QueryRunner(DataSource ds):用于根据数据源ds创建QueryRunner对象
  • int update(Connection conn,String sql, Object… params):用于执行增加、删除和更新等数据操作,其中传入的参数 params 会赋值到SQL语句的占位符中,并根据数据库连接对象执行SQL语句
  • int update(String sql, Object… params):用于执行增加、删除和更新等数据操作,和上一行方法的区别在于它需要从构造方法的数据源DataSource中获得 Connection对象
  • query(Connection conn, String sql, ResultSetHandler<T> rsh,Object… params) :用于执行表数据的查询操作,其中传入的params参数会赋值到SQL语句的占位符中,并使用结果集处理器rsh处理查询结果
  • query(String sql, ResultSetHandler<T> rsh, Object… params) :用于执行表数据的查询操作,与上一行方法的区别在于它需要从构造方法的数据源DataSource中获得 Connection对象。

9.3.2 ResultSetHandler接口

ResultSetHandler接口是一个用于处理JDBC查询操作结果集的接口,它定义了一系列方法来将结果集中的数据转换为特定的对象或数据结构,从而使程序能够更方便地处理和使用这些数据。

为了满足对结果集进行多种形式的封装的需求,Dbutils中提供了很多ResultSetHandler接口的实现类

  • ArrayHandler:将结果集中的第一条记录封装到一个Object[]数组中,数组中的元素分别表示该记录中每一个字段的值。
  • ArrayListHandler:将结果集中的所有记录封装到List<Object[]> 集合中,其中每个 Object[] 数组表示一条记录。
  • BeanHandler:将结果集中第一条记录封装到一个指定的JavaBean中。
  • BeanListHandler:将结果集中的每一条记录封装到指定的JavaBean中,再将这些JavaBean封装到List集合中。
  • ColumnListHandler:将结果集中指定字段的值封装到一个List集合中。
  • ScalarHandler:将结果集的第一行第一列封装到一个Object对象中。

9.3.3 快速上手

  1. 准备数据库连接对象
  2. 创建QueryRunner对象
  3. 执行SQL,使用ResultSetHandler接口实现类处理结果集
public class TestDBUtils {
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) throws SQLException {
        // 获取数据库连接
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        String sql = "select * from t_space where id = 1;";

        // 创建QueryRunner对象
        QueryRunner queryRunner = new QueryRunner();
        // 执行SQL,使用ResultSetHandler处理结果集
        Space space = queryRunner.query(conn, sql, new BeanHandler<>(Space.class));
        System.out.println(space);
    }
}

9.3.3 增删改查(练习)

1)准备工作

数据库:

-- 创建表
CREATE TABLE IF NOT EXISTS t_employee (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工ID',
    name VARCHAR(50) NOT NULL COMMENT '姓名',
    age INT COMMENT '年龄',
    department VARCHAR(50) COMMENT '部门',
    salary DECIMAL(10, 2) COMMENT '工资',
    INDEX idx_department (department)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息表';

-- 初始化员工数据
INSERT INTO t_employee (name, age, department, salary) VALUES
('张三', 28, '技术部', 15000.00),
('李四', 32, '技术部', 25000.00),
('王芳', 26, '人事部', 8000.00),
('刘明', 35, '销售部', 18000.00),
('陈静', 29, '财务部', 12000.00),
('赵强', 31, '技术部', 14000.00),
('孙丽', 27, '市场部', 9000.00),
('周伟', 33, '技术部', 13000.00);

-- 查询验证
SELECT * FROM employees;

实体类

package com.example.simple.entity;

/**
 * 简化的员工实体类
 */
public class Employee {
    private Integer id;         // 员工ID
    private String name;        // 姓名
    private Integer age;        // 年龄
    private String department;  // 部门
    private Double salary;      // 工资
  	
  	// 补充以下方法
    // 无参构造
    // 有参构造
    // Getter 和 Setter
  
    
}
2)增删改

基本类

public class TestDBUtils2 {
    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "123456";
    // 获取数据库连接
    private static final Connection conn;

    static {
        try {
            conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
  	// 创建QueryRunner对象
    private static QueryRunner queryRunner = new QueryRunner();
    
  	// main方法
  	public static void main(String[] args) throws SQLException {
        TestDBUtils2 dbUtils = new TestDBUtils2();
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee(100, "Tom", 26, "技术部",8000.0));
        employees.add(new Employee(101, "Jerry", 28, "运维部",6000.0));
        dbUtils.batchAddEmployees( employees);
    }
  
}

以下方法都编写在TestDBUtils2类中,作为实例方法(或使用Junit测试库)

/**
 * 添加员工
 * 要求:使用update方法实现批量插入,返回成功插入的数量
 */
public int batchAddEmployees(List<Employee> employees) {
    // TODO: 实现此方法
    return 0;
}
/**
 * 删除员工
 * 要求:删除指定姓氏的员工
 */
public int deleteEmployee(String lastName) {
    // TODO: 实现此方法
    return 0;
}
/**
 * 为指定部门的员工涨薪
 * raisePercentage传入的是涨薪百分点,1.1表示10%
 */
public int giveRaiseToDepartment(String department, double raisePercentage) {
    // TODO: 实现此方法
    return 0;
}
5)查询
/**
 * 查询年龄范围在20-30之间的员工
 * 要求:使用BeanListHandler返回Employee对象列表
 */
public List<Employee> findEmployeesByAgeRange(int minAge, int maxAge) {
    // TODO: 实现此方法
    return null;
}
/**
 * 查询指定部门的员工,并按照工资降序排序
 * 要求:使用BeanListHandler,包含排序功能
 */
public List<Employee> findEmployeesByDepartmentOrderBySalaryDesc(String department) {
    // TODO: 实现此方法
    return null;
}
/**
 * 查询工资高于平均工资的员工
 * 要求:需要先查询平均工资,再查询高于此工资的员工
 */
public List<Employee> findEmployeesAboveAverageSalary() {
    // TODO: 实现此方法
    return null;
}
/**
 * 模糊查询员工(按姓名)
 * 要求:使用参数化查询防止SQL注入
 */
public List<Employee> searchEmployeesByName(String keyword) {
    // TODO: 实现此方法
    return null;
}
/**
 * 为指定部门的员工涨薪
 * raisePercentage传入的是涨薪百分点,1.1表示10%
 */
public int giveRaiseToDepartment(String department, double raisePercentage) {
    // TODO: 实现此方法
    return 0;
}
5)查询
/**
 * 查询年龄范围在20-30之间的员工
 * 要求:使用BeanListHandler返回Employee对象列表
 */
public List<Employee> findEmployeesByAgeRange(int minAge, int maxAge) {
    // TODO: 实现此方法
    return null;
}
/**
 * 查询指定部门的员工,并按照工资降序排序
 * 要求:使用BeanListHandler,包含排序功能
 */
public List<Employee> findEmployeesByDepartmentOrderBySalaryDesc(String department) {
    // TODO: 实现此方法
    return null;
}
/**
 * 查询工资高于平均工资的员工
 * 要求:需要先查询平均工资,再查询高于此工资的员工
 */
public List<Employee> findEmployeesAboveAverageSalary() {
    // TODO: 实现此方法
    return null;
}
/**
 * 模糊查询员工(按姓名)
 * 要求:使用参数化查询防止SQL注入
 */
public List<Employee> searchEmployeesByName(String keyword) {
    // TODO: 实现此方法
    return null;
}

更多推荐