JDBC简易入门

80 阅读20分钟

JDBC

1. JDBC 概述

在JAVA 中要如何去操作数据库呢?

答案就是 通过 JDBC

JDBC 是什么?

  • "Java Database Connectivity " 即 JAVA 的数据库连接技术
  • JDBC 是 JAVA 规定的,连接数据库的接口。具体的实现由,各个数据库厂商去实现
  • 身为 java 开发人员,我们只需要知道规范和使用方法即可,不必了解内部的实现。

JDBC 接口位于 JAVA 哪个包下?

答:在 java.sql 、javax.sql 下

jdbc概述.png

JDBC体系结构:

  • 面向应用的API:Java API,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结果)。保存在 java.spljavax.spl
  • 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用。我们不必了解

JDBC 是很明显的 面向接口编程

JDBC是sun公司提供一套用于数据库操作的接口,java程序员只需要面向这套接口编程即可。

不同的数据库厂商,需要针对这套接口,提供不同实现。不同的实现的集合,即为不同数据库的驱动。 ————面向接口编程

涉及具体核心类和接口

  • DriverManager负责注册驱动
    • 将第三方数据库厂商的实现驱动 jar 注册到程序中
    • 可以根据数据库连接信息获取 connection
  • Connection负责连接数据库
    • 和数据库建立的连接,在连接对象上可以多次执行数据库curd动作
    • 可以获取 statementpreparedstatementcallablestatement 对象
  • Statement(静态sql,没有动态值语句) | PreparedStatement (动态sql,有动态值语句)|CallableStatement(执行标准存储过程SQL路线)负责将SQL 语句发送到 数据库上
    • 具体发送 SQL 语句到数据库管理软件的对象。
    • 不同发送方式稍有不同! preparedstatement 使用为重点!
  • Result存储 SQL 返回的结果集
    • 面向对象思维的产物(抽象成数据库的查询结果表)
    • 存储 DQL 查询数据库结果的对象
    • 需要我们进行解析,获取具体的数据库数据

2. JDBC 实现的步骤

要实现 JDBC 那只需要六步。

  1. 注册驱动的 jar 包进行安装 我们需要引入,sql 厂商的 实现 jar 包,这样才能实现 数据库的连接
  2. 创建链接 我们引入 jar 包之后就可以创建连接了,其实就是创建一个 connection 对象。
  3. 创建发送SQL语句的对象 创建连接之后我们就需要一个对象,能够帮助我们将SQL发送到 数据库中。这个对象就是statement
  4. 发送SQL语句,获取结果 利用上面生成的statement 对象,发送SQL 语句到数据库
  5. 解析返回结果 此时我们将得到一个resultset对象,如果是select 语句,那么就会返回对应的数据,如果是其他语句,那么就只会返回INT类型的数据
  6. 销毁资源 此时,我们需要将之前使用的对象resultstatementconnection进行销毁

看个例子:

package com.jdbctest.api.statament;


import com.mysql.cj.jdbc.Driver;

import java.sql.*;

/**
 * ClassName: StatementQueryPart
 * Package: com.jdbctest.api.statament
 * Description:
 *
 * @Author arzhu
 * @Create 2023/7/27 20:37
 * @Version 1.0
 */
public class StatementQueryPart {
    public static void main(String[] args) throws SQLException {
        //1. 注册驱动
        /*
        * 如果使用的是 mysql8.0 以下,那么此处的 new Driver 是 com.mysql.jdbc.Driver 包下的
        * 如果是 mysql8.0 那么就是 com.mysql.cj.jdbc.Driver
        * */
        DriverManager.registerDriver(new Driver());
        //2. 获取链接
        /*
        * 参数1:URL : 格式为:jdbc:数据库厂家://ip地址:端口/数据库名
        * 参数2:USER : 账户
        * 参数1:PASSWORD : 密码
        * */
        Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/jdbctest","root","abc123");
        //3. 创建 statment
        Statement statement = connection.createStatement();
        //4. 发送sql语句,并且返回结果
        String sql = "SELECT * FROM t_user";
        // 执行 查询语句
        ResultSet resultSet = statement.executeQuery(sql);
        //5. 进行结果解析
        // result 是一行一行接收的数据,所以我们要使用 result.next 使其读取下一行数据
        while (resultSet.next()){
            int id = resultSet.getInt("id");
            String account = resultSet.getString("account");
            String password = resultSet.getString("password");
            String nickName = resultSet.getString("nickname");
            System.out.println(id+"--"+account+"--"+password+"--"+nickName);
        }
        //6. 关闭资源
        resultSet.close();
        connection.close();
        statement.close();
    }
}

执行之后就能看到 数据库的全部数据了

3. Statment的问题以及JDBC的细节

需求:从键盘输入账号密码,读取数据库的账号跟密码判断是否能够成功

代码为:

package com.jdbctest.api.statament;

import com.mysql.cj.jdbc.Driver;

import java.sql.*;
import java.util.Properties;
import java.util.Scanner;

/**
 * ClassName: StatementUserLoginPart
 * Package: com.jdbctest.api.statament
 * Description:
 * 需求:从键盘输入账号密码,读取数据库的账号跟密码判断是否能够成功
 * @Author arzhu
 * @Create 2023/7/28 14:06
 * @Version 1.0
 */
public class StatementUserLoginPart {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        //0. 准备工作
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入账号:");
        String account = scanner.nextLine();
        System.out.println("请输入密码:");
        String password = scanner.nextLine();

        //1. 注册驱动
        /**
         * DriverManager.registerDriver 底层调用了一个 registerDriver 注册驱动的方法,
         * 但是,在 new Driver 上的静态代码块也调用了一个 registerDriver
         * 这就意味着 驱动调用了两次!!!形成了很大的资源浪费
         * 如果想要改进的话,我们只需要去调用一次 registerDriver
         * 考虑是使用 DriverManager.registerDriver 还是 new Driver() 呢?
         * 很明显,我们只能使用 new Driver()。因为 DriverManager.registerDriver 必须要用到 new Driver。
         *
         */
        /**
         * 静态代码块是随着类的加载而执行的,并且只会执行一次
         * 类的加载机制:(类一旦加载那么必定会执行静态代码块)
         *          加载 [class 文件 ---> jvm 的虚拟机对象]
         *          链接 [验证(检查文件类型,语法)-> 准备(静态变量 默认值)->(触发静态代码块)]
         *          初始化(静态属性赋真实值)
         *    // 这里可以去 F:\javacode\JavaSeCode\chatpter18_reflect\src\com\arzhu\doc 找到对应的文档
         *  触发静态代码块的方法(触发类加载机制的做法):
         *          1. new 关键字
         *          2. 调用静态方法
         *          3. 调用静态属性
         *          4. 接口 1.8后往接口添加 default 实现
         *          5. 反射
         *          6. 子类触发父类
         *          7. 程序的入口 main
         */
        //方案一:注册了两次 registerDriver
//        DriverManager.registerDriver(new Driver());
        // 方案二:
        /*
        * 非常不雅 并且 没有适配性
        * 此时我们导入的是 com.mysql.cj.jdbc.Driver 的 Driver
        * 如果我们导入的是 oracle 数据库呢?那么 这个包的路径又要变一次,不适合我们后续做 数据库的迁移
        * */
//        new Driver();
        // 方案三:使用 反射 可以将 配置有关的,都提取成 配置文件
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 获取链接
        /**
         * getConnection 是一个重载的方法
         * 方法一:三个参数(推荐,更加灵活)
         *      参数1:url String ---> jdbc:jdbc:数据库厂家://ip地址:端口/数据库名?key1=value1&key2=value2
         *              如果默认 ip 是主机 端口是3306 那么可以简写成 jdbc:mysql///数据库名。
         *              举个例子:此项目为:jdbc:mysql://127.0.0.1:3306/jdbctest
         *              那么可以简写为:jdbc:mysql:///jdbctest
         *              后面的 key1 和 value1 等都是附加信息。可选可不选
         *             具体有:
         *                  serverTimezone=AsiaShanghai(8.0.25以上可自动识别时区)
         *                  useUnicode=true(是否使用unicode编码格式,8.0版本以后默认)
         *                  characterEncoding=utf8(是否使用 utf8编码,8.0版本以后默认)
         *                  useSSL=true(是否忽略SSL,8.0版本以后默认)
         *      参数2:user String ---> 账号
         *      参数3:password String ---> 密码
         * 方法二:两个参数
         *       参数1:url String 等同于上面
         *       参数2:info Properties 类型,也就是一个"user=xxx" "password=yyy" 的 Properties类型数据
         *
         * 方法三:一个参数
         *        参数:url String 等同于上,但是需要添加 "user=xxxx$password=yyy"
         */
        // 方法一(推荐)
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest","root","abc123");
        // 方法二(不推荐)
//        Properties info = new Properties();
//        info.put("user","root");
//        info.put("password","abc123");
//        Connection connection1 = DriverManager.getConnection("jdbc:mysql:///jdbctest",info);
        // 方法三
//        Connection connection2 = DriverManager.getConnection("jdbc:mysql:///jdbctest?user=root&password=abc123");
        //3. 创建 statment
        Statement statement = connection.createStatement();

        //4. 发送sql语句,并且返回结果
         String sql = "SELECT * FROM t_user WHERE account = '"+account+"' AND `password` = '"+password+"';";
        // 执行 查询语句
        System.out.println(sql);
        /**
         * DML:INSERT、UPDATE、DELETE、SELECT 等操作数据的语言
         * DDL:REATE、ALTER、DROP 等定义更改数据库的语言
         * DCL:用于控制不同用户的权限的语言
         * TCL:事务控制语言,用于处理数据库中的事务。
         * DQL:主要负责查询数据的语言。
         * statement 有两个执行 sql 的方法
         * executeUpdate:只执行没有返回数据的sql,例如:DDL相关,INSERT、UPDATE、DELETE 等
         *                返回类型为 INT。
         *                情况一:DML(除了 SELECT) 返回 影响的行数
         *                情况二:非DML 返回0
         * executeQuery:执行 SELECT 的语言,返回是一个 ResultSet 类型。只接收 DQL 参数
         */
        ResultSet resultSet = statement.executeQuery(sql);
        //5. 进行结果解析
        /**
         * resultSet 得到的是一个结果集
         * resultSet 有一个指针,默认指针指向第一条数据之前
         * resultSet.next() 返回一个 boolean 类型的数据,如果 指针下方还有数据
         *                   有数据,返回 true,并且将指针指向下一条数据
         *                   否则返回 false
         *                   通常做法就是 while(resultSet.next()){执行代码....}
         * resultSet 得到数据:
         *      使用 resultSet.get类型(String 列的别名或列名 | int 列的索引 从1开始)
         */
//        while (resultSet.next()){
//            int id = resultSet.getInt("id");
//            String account1 = resultSet.getString(2);
//            String password1 = resultSet.getString(3);
//            String nickName = resultSet.getString("nickname");
//            System.out.println(id+"--"+account1+"--"+password1+"--"+nickName);
//        }
        if(resultSet.next()){
            System.out.println("登录成功!!!");
        }else{
            System.out.println("登录失败!!!");
        }
        //6. 关闭资源
        resultSet.close();
        statement.close();
        connection.close();
    }
}

  • 我们使用反射进行驱动的注册,目的是为 更灵活,可配置
  • 建立 connection 连接,我们使用 三个参数的getConnection,目的是: 更灵活,可配置
  • 由于输入的是SELECTDQL,所以我们使用executeQuery发送sql

上述代码中还存在着以下问题

  1. sql 只能使用 字符串拼接的方式,非常不雅观和不方便

  2. 只能拼接字符串类型,其他类型无能为力

  3. 可能发生SQL注入!!! 尝试将密码输入变为:

    'OR '1'='1
    

    执行结果之后发现登录成功,实际上的sql变成了:

    SELECT * FROM t_user WHERE account = 'root' AND `password` = ''OR '1'='1';
    

那么我们要如何解决这些问题呢?

答案就是:使用 PreparedStatement

4. 使用 PreparedStatment解决SQL注入

直接看代码:

public class PreparedStatementUserLoginPart {
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入账号:");
        String account = scanner.nextLine();
        System.out.println("请输入密码:");
        String password = scanner.nextLine();
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest","root","abc123");
        String sql = "SELECT * FROM t_user WHERE account = ? AND `password` = ?;";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setObject(1,account);
        preparedStatement.setObject(2,password);
        ResultSet resultSet = preparedStatement.executeQuery();
        if(resultSet.next()){
            System.out.println("登录成功!!!");
        }else{
            System.out.println("登录失败!!!");
        }
        //6. 关闭资源
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
}

密码尝试输入:'OR '1'='1

测试一下,登录失败, PreparedStatment预防了SQL注入!!!

对比一下statementPreparedStatement

  • PreparedStatement 在创建时就传入了sql,使用? 替代我们传入的变量accountpassword
  • PreparedStatement 调用setObject(index,column)方法去设置 ?处的值,按照索引的值去设置对应的值,索引从 1 开始。
  • preparedStatement.executeQuery 支持DQL 如果是除了SELECT以外的DML那么就是使用preparedStatement.executeUpdate`

原理是啥呢?

stament对比.png

5. 使用PreparedStatment执行DML

我们接下来尝试使用PreparedStatment 去实现 DML

package com.jdbctest.api.statament;

import org.junit.Test;

import java.sql.*;
import java.util.*;

/**
 * ClassName: PrepareStatementUserDML
 * Package: com.jdbctest.api.statament
 * Description:
 *
 * @Author arzhu
 * @Create 2023/7/28 18:03
 * @Version 1.0
 */
public class PrepareStatementUserDML {
    /**
     * 需求: 插入一条数据,test 123 testname
     * @throws ClassNotFoundException
     */
    @Test
    public void testInsert() throws Exception {

        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
        //3. 创建statement
        String sql = "INSERT t_user(account,`password`,nickname) VALUES(?,?,?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setObject(1,"test");
        preparedStatement.setObject(2,123);
        preparedStatement.setObject(3,"testname");
        //4. 传输SQL 获取结果
        // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
        int i = preparedStatement.executeUpdate();  // 返回的是 影像的行数

        // 5. 解析结果
        if(i==1){
            System.out.println("插入成功");
        }else{
            System.out.println("插入失败");
        }
        // 6. 关闭连接
        connection.close();
        preparedStatement.close();

    }

    /**
     * 需求:删除 id = 3 的数据
     */
    @Test
    public void testDelete() throws Exception {
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
        //3. 创建statement
        String sql = "DELETE FROM t_user WHERE id = 3";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

//        preparedStatement.setObject(1,"大狗子");
        //4. 传输SQL 获取结果
        // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
        int i = preparedStatement.executeUpdate();  // 返回的是 影像的行数

        // 5. 解析结果
        if(i==1){
            System.out.println("删除成功");
        }else{
            System.out.println("删除失败");
        }
        // 6. 关闭连接
        connection.close();
        preparedStatement.close();
    }

    /**
     * 修改 id = 3 nickname 为 大狗子
     */
    @Test
    public void testUpdate() throws Exception {

        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
        //3. 创建statement
        String sql = "UPDATE t_user SET nickname = ? WHERE id = 3";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setObject(1,"大狗子");
        //4. 传输SQL 获取结果
        // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
        int i = preparedStatement.executeUpdate();  // 返回的是 影像的行数

        // 5. 解析结果
        if(i==1){
            System.out.println("修改成功");
        }else{
            System.out.println("修改失败");
        }
        // 6. 关闭连接
        connection.close();
        preparedStatement.close();
    }

    /**
     * 需求:将数据库中的数据变成 List<Map>的形式
     *  每一个List 存储 一个 Map Map 中是这样的 key = 列, value = 值
     */
    @Test
    public void testSelectt() throws Exception {
//        准备
        List resultList = new ArrayList();
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
        //3. 创建statement
        String sql = "SELECT * FROM t_user";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        //4. 传输SQL 获取结果
        // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
        ResultSet resultSet = preparedStatement.executeQuery();// 返回的是 影像的行数

        // 5. 解析结果
       while (resultSet.next()){
           Map rowMap = new HashMap();

           //        存储单个的 Map
           int id = resultSet.getInt("id");
           String account = resultSet.getString("account");
           String password = resultSet.getString("password");
           String nickname = resultSet.getString("nickname");
           rowMap.put("id",id);
           rowMap.put("account",account);
           rowMap.put("password",password);
           rowMap.put("nickname",nickname);
           resultList.add(rowMap);


       }
        // 6. 关闭连接
        connection.close();
        preparedStatement.close();
        // 输出
        Iterator iterator = resultList.iterator();
       while (iterator.hasNext()){
           System.out.println(iterator.next());
       }


    }
}

从上面我们可以看到,对于SELECT,我们使用executeQuery 去执行SQL

对于其他的DML来说,我们使用executeUpdate执行 SQL

但是上面还是优点小问题的。

  • testSelect这段语句中,我们存入 Map 是手动去存的,这么做很蠢,也不利于维护

**testSelect**改良版

/**
     * 需求:将数据库中的数据变成 List<Map>的形式
     *  每一个List 存储 一个 Map Map 中是这样的 key = 列, value = 值
     */
    @Test
    public void testSelectt() throws Exception {
//        准备
        List resultList = new ArrayList();
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
        //3. 创建statement
        String sql = "SELECT * FROM t_user";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        //4. 传输SQL 获取结果
        // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
        ResultSet resultSet = preparedStatement.executeQuery();// 返回的是 影像的行数
//        获取列信息
        ResultSetMetaData metaData = resultSet.getMetaData();
        int columnCount = metaData.getColumnCount();  // 获取列的行数
        // 5. 解析结果
       while (resultSet.next()){
           Map rowMap = new HashMap();
//           方案1 不完美
           //        存储单个的 Map
//           int id = resultSet.getInt("id");
//           String account = resultSet.getString("account");
//           String password = resultSet.getString("password");
//           String nickname = resultSet.getString("nickname");
//           rowMap.put("id",id);
//           rowMap.put("account",account);
//           rowMap.put("password",password);
//           rowMap.put("nickname",nickname);
//           resultList.add(rowMap);

//           方案2 我们对列进行循环取到对应的 key 和 value 再存进去
//           注意这里!!!必须要从 1 开始读取,因为 数据库的索引都是从 1 开始的
           for (int i = 1; i <= columnCount; i++) {
               rowMap.put(metaData.getColumnLabel(i),resultSet.getObject(i));
           }
           resultList.add(rowMap);
       }
        // 6. 关闭连接
        connection.close();
        preparedStatement.close();
        // 输出
        Iterator iterator = resultList.iterator();
       while (iterator.hasNext()){
           System.out.println(iterator.next());
       }


    }

我们使用 resultSet.getMetaData获取列的信息,再从这个对象中获取获取列的个数getColumnCount,再通过metaData.getColumnLabel(i)获取第i个列的信息。从而实现了,智能赋值

6. JDBC 高级应用

6.1 主键回显

这个意思就是 插入数据时,得到该条数据的自增长主键

应用举例:表A为订单表(主表),表B为订单项表(子表),此时新增了一个订单,于是我们往 订单表中插入数据,但是也要往订单项表中插入数据,此时,我们必须要

  1. 先插入 订单表 拿到自增主键
  2. 拿着主键再去 订单项表 插入数据

看看例子:

package com.jdbctest.api.statament.com.jdbctest.api.preparedStatement;

import org.junit.Test;

import java.sql.*;

/**
 * ClassName: PSOtherPart
 * Package: com.jdbctest.api.statament.com.jdbctest.api.preparedStatement
 * Description:
 *  主键回显
 * @Author arzhu
 * @Create 2023/7/28 20:36
 * @Version 1.0
 */
public class PSOtherPart {
    /**
     * 需求:
     *      插入一条数据,并且拿到该条数据的 主键
     */
    @Test
    public void returnPrimaryKey() throws Exception {

        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
        //3. 创建statement
        String sql = "INSERT t_user(account,`password`,nickname) VALUES(?,?,?)";

//        ①:此时,我们需要在 connection.prepareStatement 传入第二个参数,此参数告知 preparedStatement 在返回时需要带上 主键
        PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
        preparedStatement.setObject(1,"马丫丫");
        preparedStatement.setObject(2,"abc123");
        preparedStatement.setObject(3,"myy");
        //4. 传输SQL 获取结果
        // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
        int i = preparedStatement.executeUpdate();  // 返回的是 影像的行数

        // 5. 解析结果
        if(i==1){
            System.out.println("修改成功");
            // 此时需要去获取主键 preparedStatement.getGeneratedKeys 得到一个 ResultSet 结果集
            ResultSet pk = preparedStatement.getGeneratedKeys();
            // 指针指向的是 第一项以前的,所以我们必须要 next 一下
            pk.next();
            // 此时通过 pk.getInt 就能拿到 主键了
            System.out.println("主键是:"+pk.getInt(1));
        }else{
            System.out.println("修改失败");
        }
        // 6. 关闭连接
        connection.close();
        preparedStatement.close();
    }
}

6.2 批量插入数据的优化

在我们处理批量插入数据时,往往会比较耗费资源,那么我们怎么做才能进行优化呢?

需求:插入一万条数据,测试耗费的时间

先看普通的插入:

    @Test
    public void plainInsert() throws Exception {
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
        //3. 创建statement
        String sql = "INSERT t_user(account,`password`,nickname) VALUES(?,?,?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            preparedStatement.setObject(1,"马丫丫"+i);
            preparedStatement.setObject(2,"abc123"+i);
            preparedStatement.setObject(3,"myy"+i);
            // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
            preparedStatement.executeUpdate();  // 返回的是 影像的行数
        }
        long end = System.currentTimeMillis();
        System.out.println("插入耗费的时间是:"+(end - start));
        //4. 传输SQL 获取结果



        // 6. 关闭连接
        connection.close();
        preparedStatement.close();
    }

执行一下:

插入耗费的时间是:21258

看看优化的插入:

    /**
     * 改进的插入
     * ①:创建 Connection 对象时,传入的 URL 添加 rewriteBatchedStatements=true
     * ②:sql 语句 不能带有 ';'号
     * ③:setObject之后 使用 preparedStatement.addBatch() 
     * ④:使用 executeBatch 发送 SQL
     */
    @Test
    public void improveInsert() throws Exception{
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest?rewriteBatchedStatements=true", "root", "abc123");
        //3. 创建statement
        String sql = "INSERT t_user(account,`password`,nickname) VALUES(?,?,?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            preparedStatement.setObject(1,"马丫丫"+i);
            preparedStatement.setObject(2,"abc123"+i);
            preparedStatement.setObject(3,"myy"+i);
            // 使用 addBatch 将 SQL 的values 进行一个拼接 类似于:VALUES(x,x,x),(y,y,y),(z,z,z)....
            preparedStatement.addBatch();
        }
        // 使用 executeBatch 将拼接好的 SQL 一次性发送到 MYSQL 去
        preparedStatement.executeBatch();
        long end = System.currentTimeMillis();
        System.out.println("插入耗费的时间是:"+(end - start));
        // 6. 关闭连接
        connection.close();
        preparedStatement.close();
    }

看看执行结果:

插入耗费的时间是:229

步骤:

  1. 创建 Connection 对象时,传入的 URL 添加 rewriteBatchedStatements=true
  2. sql 语句 不能带有 ';'号
  3. setObject之后 使用 preparedStatement.addBatch()
  4. 使用 executeBatch 发送 SQL

原理:

数据库的优化策略之一,就是要减少 JAVA 与 mysql 的交互,即IO。普通插入,在 for 中,setObject 之后 就将该SQL 发送到 MYSQL 中,for了10000次,那么就会产生10000次交互。而优化的插入则是采用SQL拼接的方式,将10000条数据变成一条SQL(利用了 INSERT t_(col1,col2,clo3) VALUES(y,y,y),(x,x,x),(z,z,z)....) 发送到 mysql 中,交互只发生了一次。

  • sql 语句 不能带有 ';'号,正式为了 进行 SQL的拼接做的准备
  • addBatch()方法正是拼接 SQL
  • executeBatch 作用是 发送拼接好的 SQL

6.3 JDBC 实现事务

6.3.1 事务回顾

什么是事务?

事务(Transaction)是数据库操作的基本单位,通常包含了一组操作:

要么这组操作全部执行并且数据持久化到数据库中,要么如果某个操作失败则全部操作不执行,数据库状态回滚到执行事务之前的状态。

举个例子就像转账一样;A余额 50,B 余额 20,A 需要转账10块到 B,那么完整的转账流程是:A - 10、B + 10,这个完整的流程就是一个事务。

事务的特点ACID

  1. 原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么全部完成,要么全部不完成。 就像上面提到的转账例子,如果 A - 10 成功,B + 10 失败,那么这个事务就是失败的,此时需要回滚到转账之前的状态
  2. 一致性(Consistency):事务必须使数据库从一个一致性状态变到另一个一致性状态。从上面那个转账例子来说:A 和 B 的总和是:50 + 20 = 70,就算转账成功了 它们的总和也是70,就算转账失败,它们的总和也理应是 70。
  3. 隔离性(Isolation):事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  4. 持久性(Durability):一旦事务完成,则它对数据库中的改变就是永久性的。接下来的其他操作或者系统故障不应对其有任何影响。

**事务场景:**一个业务涉及到多条修改的数据库语句,比如转账

事务要求:事务的一组操作,必须要在一个连接中!!!

事务类型:

  • 自动提交:每条语句自动存储在一个事务中,执行成功就提交,执行失败就自动回滚
  • 手动提交: 手动开启事务,添加语句,手动提交或者手动回滚。

sql开启事务

针对自动提交(推荐): 关闭自动提交,多条语句添加之后,最终手动提交或者手动回滚

  1. 执行SET autocommit = off; 关闭自动提交(只在当前连接中有效)
  2. 执行多条语句
  3. 根据执行结果选择,提交或者回滚。

**针对手动提交(不推荐):**开启事务代码,添加SQL,手动提交或者回滚。

**一般而言,我们在 java 中 执行事务是 使用try catch **

  • try存放 多条 SQL 语句,最后一段为提交
  • catch 存放 回滚语句
try{
    connection.setAutoCommit(false); // 关闭自动提交
    多条SQL
    connection.commit  // 提交
} catch{
    connection.rollback();  // 回滚
}

6.3.2 JDBC 实现事务

就从上面的例子 进行 一个 业务中的事务的操作。

前期准备:


CREATE TABLE t_bank(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '账号主键',
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
money INT UNSIGNED COMMENT '金额不能为负数'
);

INSERT t_bank(account,money) VALUES('张三',1000),('李四',2000);

需求:实现 张三 向 李四转账 500,直至 张三没钱。

  • Service层:存放业务代码
  • DAO层:存放 跟数据库打交道的代码,即 SQL 的执行。一般来说是一个表对应一个DAO

DAO层 BankDao.class

package com.jdbctest.api.statament.transaction.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

/**
 * ClassName: BankDAO
 * Package: com.jdbctest.api.statament.transaction.dao
 * Description:
 * 操作 数据库的类 DAO 包(存储DAO类)
 * @Author arzhu
 * @Create 2023/7/29 13:53
 * @Version 1.0
 */
public class BankDAO {
    /**
     * 针对 account 加 money 操作
     * @param account 账号
     * @param money 金钱
     */

    public void add(String account,int money,Connection connection) throws Exception {

        //3. 创建statement
        String sql = "UPDATE t_bank SET money = money + ? WHERE account = ?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setObject(1,money);
        preparedStatement.setObject(2,account);
        //4. 传输SQL 获取结果
        int i = preparedStatement.executeUpdate();  

        // 5. 解析结果
        if(i==1){
            System.out.println("加钱成功");
        }else{
            System.out.println("加钱失败");
        }

        preparedStatement.close();
    }
    /**
     * 针对 account 减 money 操作
     * @param account 账号
     * @param money 金钱
     */
    public void sub(String account,int money,Connection connection) throws Exception {
        //3. 创建statement
        String sql = "UPDATE t_bank SET money = money - ? WHERE account = ?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setObject(1,money);
        preparedStatement.setObject(2,account);
        //4. 传输SQL 获取结果

        int i = preparedStatement.executeUpdate();  

        // 5. 解析结果
        if(i==1){
            System.out.println("减钱成功");
        }else{
            System.out.println("减钱失败");
        }

        preparedStatement.close();
    }
}

Service层 BankService

package com.jdbctest.api.statament.transaction.service;

import com.jdbctest.api.statament.transaction.dao.BankDAO;
import org.junit.Test;

import java.sql.Connection;
import java.sql.DriverManager;

/**
 * ClassName: BankService
 * Package: com.jdbctest.api.statament.transaction.service
 * Description:
 *  针对业务的类 存储在 Service 下
 * @Author arzhu
 * @Create 2023/7/29 13:56
 * @Version 1.0
 */
public class BankService {
    @Test
    public void test() throws Exception {
        transfer("李四","张三",500);
    }
    /**
     *
     * 转装的业务 addAccount 加钱 subAccount 减钱
     * @param addAccount
     * @param subAccount
     * @param money
     */
    public void transfer(String addAccount,String subAccount,int money) throws Exception {
        BankDAO dao = new BankDAO();
        //1. 注册驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //2. 建立连接
        Connection connection = DriverManager.getConnection("jdbc:mysql:///jdbctest", "root", "abc123");
        try {
//            关闭自动提交
            connection.setAutoCommit(false);
            // 执行多条 sql 语句
            dao.add(addAccount,money,connection);
            dao.sub(subAccount,money,connection);
//            执行到这里证明没问题 所以要提交了
            connection.commit();
        }catch(Exception e){
//            走到这里证明SQL 出错 所以要回滚
            connection.rollback();
            throw e;
        }


    }
}

总结:

  • 每个业务就是一个事务,因为每个业务都会涉及到 多个SQL 的执行
  • 利用 try catch 进行事务的回滚
    • try 执行 SQL 完毕之后进行提交
    • catch 进行 抛错 和 回滚
  • 关闭连接要在 业务层去关闭(这点其实不优雅,可以优化一下)

7. Druid连接池

7.1 连接池概念

试想一个问题,一个 数据库连接的创建,一般来说有三个步骤

  1. 创建,时间为 t1。
  2. 使用,时间为 t2。
  3. 销毁,时间为 t3。

像上面的例子,如果我们去实现一个业务就需要创建一个连接,那么总的时间就是:业务数量 *(t1+t2+t3)

一般来说,t2 < t1 + t3。使用的耗费的资源和时间,远远小于 连接的创建和销毁。

那么能不能有一个方式,能让我们只使用连接,无需去创建和销毁连接呢?达到业务数量 * t2

有!那就是连接池设计模式。

顾名思义,连接池就是无需创建和销毁连接,要用连接就从连接池里取,取完再放回去就行

数据库连接池在Java中是一种常见的设计模式,其目的是为数据库的连接请求创建一个缓存。当应用程序需要与数据库进行交互时,它可以从连接池中获取一个已存在的连接,而不是新建一个。当应用程序完成与数据库的交互后,这个连接可以被返回到连接池,而不是被关闭,以便下次再次使用。这可以大大提高对数据库的访问速度并节省系统资源。

在 JAVA 中的连接池,都是实现了 JAVA 的接口javax.sql.DataSource

7.2 市面上常见的连接池技术

  1. HikariCP:这是一个非常快的数据库连接池,被广泛认为是目前性能最好的连接池。它的优点在于简洁的代码和零配置的默认设置,以及其在高并发环境下的优异表现。
    • 优点在于它的性能极高,而且提供了零配置的默认设置,但是对于特殊的配置需求,其选项可能较少。
  2. C3P0:C3P0是一个开源的JDBC连接池,它提供了丰富的配置选项,并能够自动恢复关闭的连接。然而,与HikariCP相比,其性能略逊一筹。
    • 优点是提供了很多配置选项,而且稳定性较高,但是性能较低。
  3. Apache DBCP:DBCP(Database Connection Pool)是Apache Commons项目的一部分,它提供了基础的连接池功能,但性能和可配置性相对较低。
    • 优点在于它作为Apache Commons项目的一部分,与许多Apache项目有很好的集成,但其性能和可配置性较低。
  4. Tomcat JDBC Connection Pool:这是Tomcat提供的一个连接池实现,它提供了一些高级功能,如验证查询和连接验证,同时保持了较高的性能。
    • 优点是提供了一些高级功能,同时保持了较高的性能,但是如果你的应用程序不是运行在Tomcat服务器上,使用这个连接池可能会有一些额外的配置工作。
  5. Druid:Druid 是阿里巴巴开源的数据库连接池,它是一个高效的并发连接管理工具,可大幅提升应用程序的数据库访问效率。它支持对于许多不同类型的数据库。
    • 优点:可配置化,性能高,扩展强,开源免费

7.3 Druid 的使用

1、我们需要导入 Druid 的 jar 包

2、创建数据库配置文件druid.properties

# 使用 key=value 的形式
# 能比 properties 读取
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=abc123
url=jdbc:mysql:///jdbctest

3、使用

    @Test
    public void testSoft() throws Exception {
//        1. 读取配置
        Properties properties = new Properties();
//        src 下的文件可以使用类的加载器去加载
        InputStream resourceAsStream = DruidTest.class.getClassLoader().getResourceAsStream("druid.properties");
        properties.load(resourceAsStream);
//        2. 使用连接池的工具类,工厂设计模式 创建连接池
        DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
//        拿出连接
        Connection connection = dataSource.getConnection();
//        回收连接
        connection.close();
    }

8. JDBC 工具类封装

我们针对上面的 事务 例子 来进行 工具类的封装

8.1 JDBC 连接池的工具类封装

package com.jdbctest.api.statament.JDBCUtils;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.jdbctest.api.statament.druid.DruidTest;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

/**
 * ClassName: JDBCUtils
 * Package: com.jdbctest.api.statament.JDBCUtils
 * Description:
 * TODO
 *  JDBC 的 utils 类
 *  负责创建 连接池
 *  提供连接对象
 *  因为 Service 层 和 DAO 层都是在一个线程
 *  所以要使用 ThreadLocal 保存连接对象
 * @Author arzhu
 * @Create 2023/7/29 19:15
 * @Version 1.0
 */
public class JDBCUtils {
    private static ThreadLocal<Connection> tl = new ThreadLocal<>();
    private static DataSource dataSource = null;
    /**
     * 静态代码块,一开始将初始化 连接池
     * 初始化 连接对象
     */
    static {
//      1. 初始化连接池
        //        1. 读取配置
        Properties properties = new Properties();
//        src 下的文件可以使用类的加载器去加载
        InputStream resourceAsStream = DruidTest.class.getClassLoader().getResourceAsStream("druid.properties");
        try {
            properties.load(resourceAsStream);
            dataSource = DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
//        2. 使用连接池的工具类,工厂设计模式 创建连接池
    }

    /**
     * 得到 connection 对象
     * @return
     * @throws SQLException
     */
    public static Connection getConnection() throws SQLException {
        Connection connection = tl.get();
        // 第一次 进入 没有 connection 对象的话 那么就从连接池里取
       if(connection == null){
           connection = dataSource.getConnection();
           tl.set(connection);
       }
       return connection;
    }

    /**
     *
     */
    public static void freeConnection() throws SQLException {
        Connection connection = tl.get();

        if(connection!=null){
//            释放资源,节省内存
            tl.remove();
//            更改为自动提交
            connection.setAutoCommit(true);
        }
        connection.close();  // 放回资源池
    }
}

  • ServiceDAO层都是在一个线程内的,所以,我们使用ThreadLocal固定住这个线程唯一的connection实例,避免了资源浪费,也优化了代码逻辑

使用:

DAO层

package com.jdbctest.api.statament.transactionnew.dao;

import com.jdbctest.api.statament.JDBCUtils.JDBCUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;

/**
 * ClassName: BankDAO
 * Package: com.jdbctest.api.statament.transaction.dao
 * Description:
 * 操作 数据库的类 DAO 包(存储DAO类)
 * @Author arzhu
 * @Create 2023/7/29 13:53
 * @Version 1.0
 */
public class BankDAO {
    /**
     * 针对 account 加 money 操作
     * @param account 账号
     * @param money 金钱
     */

    public void add(String account,int money) throws Exception {
        Connection connection = JDBCUtils.getConnection();
        //3. 创建statement
        String sql = "UPDATE t_bank SET money = money + ? WHERE account = ?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setObject(1,money);
        preparedStatement.setObject(2,account);
        //4. 传输SQL 获取结果
        // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
        int i = preparedStatement.executeUpdate();  // 返回的是 影像的行数

        // 5. 解析结果
        if(i==1){
            System.out.println("加钱成功");
        }else{
            System.out.println("加钱失败");
        }

        preparedStatement.close();
    }
    /**
     * 针对 account 减 money 操作
     * @param account 账号
     * @param money 金钱
     */
    public void sub(String account,int money) throws Exception {
        Connection connection = JDBCUtils.getConnection();
        //3. 创建statement
        String sql = "UPDATE t_bank SET money = money - ? WHERE account = ?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setObject(1,money);
        preparedStatement.setObject(2,account);
        //4. 传输SQL 获取结果
        // 由于是 DML   且不是 SELECT 所以我们使用  executeUpdate
        int i = preparedStatement.executeUpdate();  // 返回的是 影像的行数

        // 5. 解析结果
        if(i==1){
            System.out.println("减钱成功");
        }else{
            System.out.println("减钱失败");
        }

        preparedStatement.close();
    }
}

跟上面的事务例子相比,我们没有传入connection对象。而是利用线程全局存在的 connection

Service

package com.jdbctest.api.statament.transactionnew.service;

import com.jdbctest.api.statament.JDBCUtils.JDBCUtils;
import com.jdbctest.api.statament.transactionnew.dao.BankDAO;
import org.junit.Test;

import java.sql.Connection;
import java.sql.DriverManager;

/**
 * ClassName: BankService
 * Package: com.jdbctest.api.statament.transaction.service
 * Description:
 *  针对业务的类 存储在 Service 下
 * @Author arzhu
 * @Create 2023/7/29 13:56
 * @Version 1.0
 */
public class BankService {
    @Test
    public void test() throws Exception {
        transfer("李四","张三",500);
    }
    /**
     * 注意:
     *   每个业务就是一个事务,因为每个业务都会涉及到 多个SQL 的执行
     *   利用 try catch 进行事务的回滚
     *      try 执行 SQL 完毕之后进行提交
     *      catch 进行 抛错 和回滚
     *   关闭连接要在 业务层去关闭(这点其实不优雅,可以优化一下)
     *
     * 转装的业务 addAccount 加钱 subAccount 减钱
     * @param addAccount
     * @param subAccount
     * @param money
     */
    public void transfer(String addAccount,String subAccount,int money) throws Exception {
        BankDAO dao = new BankDAO();
        Connection connection = JDBCUtils.getConnection();
        try {
//            关闭自动提交
            connection.setAutoCommit(false);
            // 执行多条 sql 语句
            dao.add(addAccount,money);
            dao.sub(subAccount,money);
//            执行到这里证明没问题 所以要提交了
            connection.commit();
        }catch(Exception e){
//            走到这里证明SQL 出错 所以要回滚
            connection.rollback();
            throw e;
        }


    }
}

8.3 JDBC 的 DAO层封装

一个表对应着一个DAO层,但是很多表的 CRUD 都是一致的,所以我们要将这些逻辑提取出来,变成一个 抽象类,我们称之为BaseDAO

CRUD 可以分成两类

  • 非DQL:即INSERTDELETEUPDATE,这三个操作我们都是用executeUpdate,返回值都是影响的行数
  • DQL:即SELECT,这个操作我们是使用executeQuery,返回是一个ResultSet

BaseDAO层

package com.jdbctest.api.statament.JDBCUtils;

import com.jdbctest.api.statament.transaction.dao.User;
import org.junit.Test;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * ClassName: BaseDAO
 * Package: com.jdbctest.api.statament.JDBCUtils
 * Description:
 * TODO
 *  DAO 的基类
 *  目的就是简化 下面的 DAO 操作
 *  作用:1、从连接池中取出 连接实例
 *       2、执行SQL
 *       3、执行完毕之后,返还 连接实例
 *       4、返回结果
 * @Author arzhu
 * @Create 2023/7/29 20:22
 * @Version 1.0
 */
public abstract class  BaseDAO {
    /**
     * 执行 非 DQL 操作
     * @param sql 传入的SQL 用占位符 进行赋值
     * @param params
     * @return
     * @throws SQLException
     */
    public int executeUpdate(String sql,Object...params) throws SQLException {
        // 1. 拿到连接实例
        Connection connection = JDBCUtils.getConnection();
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // 进行SQL 的赋值
//        从1 开始
        for (int i = 1; i <= params.length; i++) {
            preparedStatement.setObject(i,params[i-1]);
        }
        int i = preparedStatement.executeUpdate();
        preparedStatement.close();
        return i;
    }

    /**
     * 每一个类对应一个表
     * 类中的属性 对应着 表中的字段
     * 返回一个 List<T>
     * 比如 我们传入一个 USER 类,那么我们执行 SQL 之后 就会创建一个 List<User>
     *
     * @param clazz  类的路径 为了获得 反射
     * @param sql    sql 语句
     * @param params 参数
     * @param <T>    泛型
     * @return
     */
    public <T> List<T> executeQuery(Class<T> clazz, String sql, Object...params) throws Exception {
//        建立一个 List<T> 接收结果集
        List<T> list = new ArrayList<>();
        // 1. 拿到连接实例
        Connection connection = JDBCUtils.getConnection();
        PreparedStatement preparedStatement = connection.prepareStatement(sql);

        // 进行SQL 的赋值
//        从1 开始 查询
        for (int i = 1; i <= params.length; i++) {
            preparedStatement.setObject(i,params[i-1]);
        }
        ResultSet resultSet = preparedStatement.executeQuery();
        ResultSetMetaData metaData = preparedStatement.getMetaData();
        int columnCount = metaData.getColumnCount();
//        解析结果集
        while (resultSet.next()){
//            创建 一个类的实例 用来接收 结果
            T t = clazz.getDeclaredConstructor().newInstance();
//            为 类中的属性赋值 采用了 反射的方法
            for (int i = 1; i <= columnCount; i++) {
                Field declaredField = clazz.getDeclaredField(metaData.getColumnName(i).toLowerCase());// 这里是因为我数据库字段名设计成了大写,所以我需要转成小写
                declaredField.setAccessible(true);  // 破坏 private 权限
                declaredField.set(t,resultSet.getObject(i));
            }
            list.add(t);
        }
        preparedStatement.close();
        return list;
    }
}

DAO层UserDAO

package com.jdbctest.api.statament.transaction.dao;

import com.jdbctest.api.statament.JDBCUtils.BaseDAO;

import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;

/**
 * ClassName: UserDAO
 * Package: com.jdbctest.api.statament.transaction.dao
 * Description:
 *
 * @Author arzhu
 * @Create 2023/7/29 20:30
 * @Version 1.0
 */
public class UserDAO extends BaseDAO {

    public void insertTest(String account,String password,String nickname) throws SQLException {
        int i = this.executeUpdate("INSERT t_user(account,password,nickname) VALUES(?,?,?)", account, password, nickname);
        if(i==1){
            System.out.println("插入成功");
        }else{
            System.out.println("插入失败");
        }

    }
    public void delTest(String account) throws SQLException {
        int i = this.executeUpdate("DELETE FROM t_user WHERE account = ?", account);
        if(i==1){
            System.out.println("删除成功");
        }else{
            System.out.println("删除失败");
        }

    }
    public void selectTest() throws Exception {
        List<User> selList = executeQuery(User.class, "SELECT * FROM t_user;");
        Iterator<User> iterator = selList.iterator();
        while (iterator.hasNext()){
            User user = iterator.next();
            System.out.println(user);
        }

    }
}

User类

package com.jdbctest.api.statament.transaction.dao;

/**
 * ClassName: User
 * Package: com.jdbctest.api.statament.transaction.dao
 * Description:
 *
 * @Author arzhu
 * @Create 2023/7/29 21:40
 * @Version 1.0
 */
public class User {
    private int id;
    private String account;
    private String password;
    private String nickname;

    public User(int id, String account, String password, String nickname) {
        this.id = id;
        this.account = account;
        this.password = password;
        this.nickname = nickname;
    }

    public User() {
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", account='" + account + '\'' +
                ", password='" + password + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
}

UserService类

package com.jdbctest.api.statament.transaction.service;

import com.jdbctest.api.statament.JDBCUtils.JDBCUtils;
import com.jdbctest.api.statament.transaction.dao.UserDAO;
import org.junit.Test;

import java.sql.SQLException;

/**
 * ClassName: UserService
 * Package: com.jdbctest.api.statament.transaction.service
 * Description:
 *  插入一条数据
 * @Author arzhu
 * @Create 2023/7/29 20:37
 * @Version 1.0
 */
public class UserService {
    @Test
    public void test1() throws Exception {
//        insertService();
//        delService();
        selService();
    }
    public void insertService() throws SQLException {
        UserDAO userDAO = new UserDAO();
        userDAO.insertTest("sdhjhf","124545","马丫丫");
        JDBCUtils.freeConnection();
    }
    public void delService() throws SQLException {
        UserDAO userDAO = new UserDAO();
        userDAO.delTest("sdhjhf");
        JDBCUtils.freeConnection();
    }
    public void selService() throws Exception {
        UserDAO userDAO = new UserDAO();
        userDAO.selectTest();
        JDBCUtils.freeConnection();
    }
}