JDBC超详细封装教程

JBDC封装(超详细)

1.前言

​ JDBC(Java Database Connectivity)是什么?它是一种规范,SUN公司要求数据库公司必须按照JDBC规范实现对应数据库Java代码,提供数据库的相关资源给java程序。在JDBC出来之前,各种数据库提供给java程序数据的方法各式各样,可以说,JDBC极大地方便了我们java开发程序员,目前和java配合最好的数据库是Oracle和MySQL数据库。

​ JDBC只是一种规范,数据库厂商根据这个标准实现自家的驱动Dirver,例如MySQL驱动com.mysql.cj.jdbc.Driver,Oracle驱动oracle.jdbc.OracleDriver,加载过驱动,我们就可以对数据库进行操作了。

2.导入第三方jar包
2.1普通java项目
	如果创建的是普通的java项目,需要手动导入第三方jar包,可以自行去官网下载。

​ jar包导入项目的方法我这里就不过多赘述了。

2.2Maven项目
	Maven是java项目统一管理工具,可以解决后期项目频繁导入jar包繁琐操作的问题,同时,如果是传统项目,jar包的更新迭代,我们还需要对整个项目的jar包进行替换,这里也可以用Maven项目来解决。
	方法:
	在pom.xml文件中写入依赖:
		如下代码:
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

​ 注释里面是这个网址,可以在里面直接复制此代码。

3.JDBC连接核心参数
1.明确连接的是哪一个数据库,提供对应驱动
	导入第三方jar包
2.url:统一资源定位符,可以明确数据库在哪
	需要数据库对应的主机名,域名和IP地址以及【端口号】
3.连接数据库的用户名和密码

可以新建一个jdbc.properties文件,将连接参数都写入进去。

driverClass=com.mysql.jdbc.Driver
jdbcUrl=jdbc:mysql://localhost:3306/gp_01?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
username=root
password=root
4.JDBC连接数据库

​ 我们用一个小Demo来看一下简单流程:

@Test
public void testInsert() {
    //1.准备数据库连接必要参数
    String jdbcUrl = "jdbc:mysql://localhost:3306/gp_01?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false";
    String username = "root";
    String password = "root";

    Connection connection = null;
    Statement statement = null;

    try {
        //2.加载驱动
        Class.forName("com.mysql.jdbc.Driver");

        //3.获取数据库连接对象
        connection = DriverManager.getConnection(jdbcUrl, username, password);

        //4.获取数据库搬运工连接对象
        statement = connection.createStatement();

        /*
        5.准备执行sql语句
         */
        String sql = "insert into gp_01.student( name, age, gender) value ('伍宇龙',16,false)";

        //6.执行sql语句
        int affectRows = statement.executeUpdate(sql);
        System.out.println("sql影像行数:" + affectRows);

    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    } finally {
        //7.关闭数据库资源
        try {
            if (statement != null) {
                statement.close();
            }
            if (connection != null) {
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

​ 上面执行的插入操作,向数据库中插入数据,由此,我们可以总结一下JDBC操作数据库执行增删查改的操作流程,如下:

1.首先,明确数据库连接的必要参数
	jdbcurl 	统一资源定位符
	driverClass	驱动类
	username	用户名
	password	密码
2.加载驱动
3.获取数据库连接对象 java.sql.Connection
4.准备目标SQL语句
5.通过Connection获得数据库搬运工对象,这里有两种对象,分别是Statement PreparedStatement
6.执行SQL语句,得到ResultSet结果集对象
7.解析ResultSet结果集
8.关闭资源

​ 为什么进行JDBC的封装,就是因为,我们每次对数据库操作如果都像上面的案例一样,每操作一次都进行一次数据库的连接,驱动加载,资源关闭会显得非常麻烦,所以,我们封装一个工具类,帮助我们更方便快捷的进行这些资源的管理。

5.资源管理工具类

我们创建资源管理工具类的目的:

  • 让资源自动加载(驱动)
  • 获取数据库连接对象
  • 数据库相关操作资源的关闭方法
package com.renlon.utils;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Arrays;
import java.util.Properties;

/**
 * @author: renlon 2023/03/14 16:56
 */
public class JdbcUtils {

    private static String jdbcUrl;
    private static String username;
    private static String password;

    static {
        //1.找到资源目录下的配置文件
        InputStream input = JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");

        //2.实例化 Properties 对象
        Properties properties = new Properties();

        try {
            //加载配置文件,完成配置文件的解析
            properties.load(input);

            //获取参数
            jdbcUrl = properties.getProperty("jdbcUrl");
            username = properties.getProperty("username");
            password = properties.getProperty("password");

            //加载驱动
            Class.forName(properties.getProperty("driverClass"));
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (input != null) {
                    input.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取连接对象
     *
     * @return java.sql.Connection 数据库连接对象
     */
    public static Connection getConnection() {
        Connection connection = null;
        try {
            connection = DriverManager.getConnection(jdbcUrl, username, password);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return connection;
    }

    /**
     * 关闭资源方法
     *
     * @param connection 数据库连接对象资源
     * @param statement  数据库 SQL 语句搬运工资源
     */
    public static void close(Connection connection, Statement statement) {
        close(statement, connection);
    }

    /**
     * 关闭资源方法
     *
     * @param connection 数据库连接对象资源
     * @param statement  数据库 SQL 语句搬运工资源
     * @param resultSet  数据库查询结果集对象
     */
    public static void close(Connection connection, Statement statement, ResultSet resultSet) {
        close(resultSet, statement, connection);
    }

    /**
     * 内部私有化方法,统一处理数据库相关资源
     *
     * @param resources 不定长参数,统一关闭对应资源
     */
    private static void close(AutoCloseable... resources) {
        if (resources != null && resources.length > 0) {
            Arrays.stream(resources).forEach(source -> {
                try {
                    if (source != null) {
                        source.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

  1. 在static静态代码块中,静态代码块随着类的加载而加载,而且只加载一次。我们通过加载配置文件,获得了必要的数据库参数,同时加载了驱动。

  2. 获取数据库连接对象,通过DriverManager.getConnection(jdbcUrl, username, password)获得connection对象。

  3. 关闭连接资源对象,这里使用了AutoCloseable 不定长参数来传入资源对象进行关闭,重载了两个close方法,分别对应了两种情况:

    ​ 第一种:关闭connection,statement对象(增删查操作)

    ​ 第二种:关闭 connection,statement,resultSet对象(查询操作)

封装增删查改之前,我先介绍一下Statement和PreparedStatement的区别:

PreparedStatement:
	预处理SQL得到PreparedStatement,支持占位参数,需要进行sql语句参数的赋值操作,可以防止sql注入的问题
	Statement 是直接通过 Connection 数据库连接对象获取,只是 SQL 语句搬运工,有可能会导致 SQL 注入问题

​ 这里建议使用PreparedStatement。

6.增删改封装(BaseDao)
增删改的操作具有一定的相似性,他们操作操作数据库的的核心步骤为:
	1.获取数据库连接对象connection
	2.准备sql语句
	3.预处理sql得到statement对象
	4.对sql语句参数进行赋值操作
	5.执行sql语句,并获得数据库表受影响的行数
	6.关闭资源

​ 他们的核心第三,第四步都是相同的,可以封装一个query方法来进行统一预处理sql,并进行sql参数的赋值操作。

预处理sql,并赋值参数得封装

/**
 * 预处理SQL,获取PreparedStatement对象,并对SQL参数进行赋值操作
 *
 * @param connection 数据库连接对象
 * @param sql        sql语句
 * @param parameters 实际参数
 * @return 返回PreparedStatement对象
 * @throws SQLException SQL异常
 */
private static PreparedStatement handlePreparedStatement(Connection connection, String sql, Object[] parameters) throws SQLException {
    //预处理 SQL 语句,得到PreparedStatement对象
    PreparedStatement statement = connection.prepareStatement(sql);

    //获取数据库元数据,并通过元数据获取SQL语句参数个数
    int parameterCount = statement.getParameterMetaData().getParameterCount();

    //条件约束,判断是否需要进行参数赋值操作
    if (parameterCount != 0 && parameters != null && parameters.length == parameterCount) {
        for (int i = 0; i < parameterCount; i++) {
            statement.setObject(i + 1, parameters[i]);
        }
    }

    return statement;
}

​ 这个方法同样适用于查询操作。

增删查的方法封装

/**
 * 更新方法,支持 insert delete update 操作
 *
 * @param sql        目标执行的 SQL 语句
 * @param parameters 对应 SQL 语句的参数,数据类型为 Object 不定长参数
 * @return SQL 语句执行对应数据表的影响行数。
 */
public int update(String sql, Object... parameters) {
    // 1. 准备必要的变量
    int affectedRows = 0;
    Connection connection = null;
    PreparedStatement statement = null;

    // 2. 利用 util.JdbcUtils 工具类获取数据库连接对象
    connection = JdbcUtils.getConnection();

    try {
        statement = handlePreparedStatement(connection, sql, parameters);
        // 5. 执行 SQL 语句,得到受影响的行数
        affectedRows = statement.executeUpdate();

    } catch (SQLException e) {
        throw new RuntimeException(e);
    } finally {
        JdbcUtils.close(connection, statement);
    }

    return affectedRows;
}
7.查询操作的封装
7.1返回值类型

​ 查询是一个重点,我们平常用到最多的就是查询,而且查询的结果也是多种多样,这里查询返回的结果可以是:

1.查询一行数据,返回目标执行SQL符合JavaBean规范的对象(对象类型多种多样,可以用泛型T表示)
2.查询多行,返回符合JavaBean规范的对象,并存储到List集合(List<T>)
3.查询单行数据,执行SQL查询到的数据存储到Map<String,Object>,字段名存储到key,value中存储字段对应的数据
4.查询多行数据,执行SQL查询到的数据存储到List<Map<String,Object>>,将上面单行Map数据,存储到List集合
5.查询单行数据,执行SQL查询到的数据存储到Object[]数组。
6.查询多行数据,将上面每单行Object[]数据存储到List集合List<Object[]>

​ 常见的形式大概有以上六种,接着就该思考该怎样封装?如果针对每一个返回值类型都去分别实现一个方法,那么可想而知,冗余代码会有很多。那么,哪部分代码是重复的?这是我们该思考的问题,很简单,我们之所以会查询到各式各样类型的返回值数据,是因为我们对结果集的处理不同,前面的步骤都是一样的。

​ 那么我们就可以考虑将ResultSet结果集处理封装成一个接口,要实现类完成接口规定的方法,从而得到不同的数据反馈。

7.2接口设计

​ 接口中方法分析:

1.参数:
	定义接口,是为了以不同方式处理结果集,所以参数为RestSet结果集对象
2.返回值类型:
	返回值类型多种多样,可以用泛型T表示
@FunctionalInterface
public interface ResultSetHandler<T> {
    /**
    * 结果集处理方法,处理ResultSet结果集
    * 
    * @param res 结果集对象
    * @return 泛型约束,由实现类决定最终的返回数据
    */
	T handle(ResultSet res);
}
7.3统一查询query

​ 上面说到查询操作除了结果集的处理不同,其他的步骤都是一样的,所以这里可以将他们封装成一个统一的方法query

/**
 * 通用 query 查询方法
 *
 * @param sql        要执行的SQL语句
 * @param rsh        结果集处理器接口实现类对象
 * @param parameters SQL语句对应参数
 * @param <T>        自定义声明泛型
 * @return 接口实现类约束的返回值类型结果
 * @throws SQLException SQL 异常
 */
public <T> T query(String sql, ResultSetHandler<T> rsh, Object... parameters) throws SQLException {
    //准备必要变量
    Connection connection = null;
    PreparedStatement statement = null;
    ResultSet resultSet = null;

    //  获取数据库连接对象
    connection = JdbcUtils.getConnection();

    // 泛型变量,具体数据类型由接口实现类决定
    T t = null;

    try {
        //上面封装的handlePreparedStatement方法,可以完成 SQL 语句预处理和参数赋值操作
        statement = handlePreparedStatement(connection, sql, parameters);

        // 执行SQL语句,得到结果集对象
        resultSet = statement.executeQuery();

        // 处理结果集对象,返回值类型由实现类决定
        t = rsh.handle(resultSet);
    } catch (SQLException e) {
        throw e;
    } finally {
        JdbcUtils.close(connection, statement, resultSet);
    }

    return t;
}

​ 方法的参数有三个,待执行的SQL语句,遵从接口实现类对象,还有SQL语句对应参数。

t = rsh.handle(resultSet);这里的泛型由接口具体的实现类来约束类型。

7.4接口实现类
我们总共需要封装6个实现类,分别是:
	BeanHandler
	BeanListHandler
	MapHandler
	MapListHandler
	ArrayHandler
	ArrayListHandler
是不是感觉也很麻烦,但是你要想他的复用度很高,而且还可以进一步简化代码,只需要实现三个就行。
7.4.1MapHandler

​ 首先,我们先来进行一个MapHandler的实现,这个比较简单易懂,我们可以先来了解一下思路:

实现类

/**
 * @author: renlon 2023/03/18 9:55
 */
public class MapHandler implements ResultSetHandler<Map<String,Object>> {
    @Override
    public Map<String, Object> handle(ResultSet res) throws SQLException {
        Map<String, Object> map;
        
        //获取结果集元数据
        ResultSetMetaData metaData = res.getMetaData();
        
        //获取查询到的结果集对应字段个数
        int columnCount = metaData.getColumnCount();
        
        //根据字段个数赋值容量,节省空间
        map = new HashMap<>(columnCount);
        
        //添加数据
        for (int i = 1; i <= columnCount; i++) {
            map.put(metaData.getColumnName(i),res.getObject(i));
        }
        
        return map;
    }

​ 这里比较重要的点是我们需要明确Map里面放的是什么,key中是数据库表的字段名称,value是对应的数据。这是比较重要的两点,那么怎么获得?

1.先获取结果集元数据res.getMetaData()

2.metaData.getColumnName(int column) 获取指定索引的字段名,注意:数据库下标一般从1开始,除了limit。

3.获取对应的值res.getObject(String columnLable)

​ 查询方法实现:

public Map<String, Object> queryMap(String sql, Object... parameters) throws SQLException {
    //调用统一query查询方法,传入接口实现类对象
    return query(sql, new MapHandler(), parameters);
}
7.4.2MapListHandler

​ 封装完MapHandler,这个就比较简单了,无非就是将多个map放入List集合中,多嵌套了一层,而且封装完MapListHandler,MapHandler的代码还可以进一步简化。他们的思路都是一样的。

public class MapListHandler implements ResultSetHandler<List<Map<String, Object>>> {
    @Override
    public List<Map<String, Object>> handle(ResultSet res) throws SQLException {
        List<Map<String, Object>> mapList = new ArrayList<>();

        //获取结果集元数据对象
        ResultSetMetaData metaData = res.getMetaData();

        //获取字段个数
        int columnCount = metaData.getColumnCount();
        while (res.next()) {
            //实例化 Map 双边队列,根据字段个数赋值容量,节省空间
            HashMap<String, Object> map = new HashMap<>(columnCount);

            for (int i = 1; i <= columnCount; i++) {
                //添加数据
                map.put(metaData.getColumnName(i), res.getObject(i));
            }
			
            //添加map到list集合
            mapList.add(map);
        }

        return mapList.isEmpty() ? null : mapList;
    }
}

​ 简化MapHandler

public class MapHandler implements ResultSetHandler<Map<String, Object>> {
    @Override
    public Map<String, Object> handle(ResultSet res) throws SQLException {
		
        //创建MapListHandler()实例处理res(结果只有一行数据)
        List<Map<String, Object>> mapList = new MapListHandler().handle(res);
        return mapList != null ? mapList.get(0) : null;
    }
}

​ 查询方法实现:

public List<Map<String, Object>> queryMapList(String sql, Object... parameters) throws SQLException {
    return query(sql, new MapListHandler(), parameters);
}
7.4.3BeanListHandler

​ 这是六个实现类里面最难的一个,考虑到JavaBean类型的多样性,需要用户指定类型来约束泛型,需要用到反射,接下来请看实现代码:

实现类

/**
 * @author: renlon 2023/03/18 10:51
 */
public class BeanListHandler<T> implements ResultSetHandler<List<T>> {

    //考虑到用户类型需要JavaBean类型的多样,需要由用户传入类型来约束泛型,需要用到反射
    private final Class<T> cls;

    public BeanListHandler(Class<T> cls) {
        this.cls = cls;
    }

    @Override
    public List<T> handle(ResultSet res) throws SQLException {
        ArrayList<T> list = new ArrayList<>();

        T t = null;

        //获取结果集元数据
        ResultSetMetaData metaData = res.getMetaData();

        int columnCount = metaData.getColumnCount();

        try {

            while (res.next()) {

                //通过反射获取实例对象
                t = cls.getConstructor().newInstance();

                for (int i = 1; i <= columnCount; i++) {
                    //添加值到JavaBean对象中
                    BeanUtils.setProperty(i, metaData.getColumnName(i), res.getObject(i));
                }
                list.add(t);
            }
        } catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
            e.printStackTrace();
        }
        return list;
    }
}

​ 思路:因为接口传入参数已经确定,那么我们该怎么获得实例对象呢?可以利用成员变量来提供反射类型变量。成员变量使用final约束,那么实例化此类必须给其赋值,这里通过有参数的构造方法来给其赋值。

​ 最终查询方法实现:

public <T> List<T> queryBeanList(String sql, Class<T> cls, Object... parameters) throws SQLException {
    /调用query查询方法,通过传入类对象来约束泛型
    return query(sql, new BeanListHandler<>(cls), parameters);
}
7.4.4BeanHandler

​ 查询单行数据,跟上面的方法一致,这里就不过多赘述,直接上代码:

实现类

public class BeanHandler<T> implements ResultSetHandler<T> {

    private final Class<T> cls;

    public BeanHandler(Class<T> cls) {
        this.cls = cls;
    }

    @Override
    public T handle(ResultSet res) throws SQLException {
        List<T> list = new BeanListHandler<>(cls).handle(res);

        return list != null ? list.get(0) : null;
    }
}

​ 最终查询方法实现:

public <T> T queryBean(String sql, Class<T> cls, Object... parameters) throws SQLException {
    //调用query查询方法,通过传入类对象来约束泛型
    return query(sql, new BeanHandler<>(cls), parameters);
}
7.4.5ArrayListHandler

​ 这里可能会有人问:为什么要用数组来存储查询到的数据库表中数据,唯一的一点优点就是:节省空间,用其他例如Map,还要在key中存储字段名称,用数组直接存储数据即可。只要和前端规定好数据的顺序即可。

实现类

public class ArrayListHandler implements ResultSetHandler<List<Object[]>> {
    @Override
    public List<Object[]> handle(ResultSet res) throws SQLException {
        //实例化List集合
        List<Object[]> list = new ArrayList<>();

        //结果集获取字段个数
        int columnCount = res.getMetaData().getColumnCount();

        while (res.next()) {
            //每一次循环对应一行数据,数组的容量为字段个数
            Object[] arr = new Object[columnCount];

            for (int i = 0; i < arr.length; i++) {
                arr[i] = res.getObject(i + 1);
            }

            list.add(arr);
        }

        return list.isEmpty() ? null : list;
    }
}

​ 这里的思路和List<Map<String,Object>>一致,而且这里更简单,只需要知道字段的个数即可,接着向数组中添加数据。

​ 最终查询方法实现:

public List<Object[]> queryArrayList(String sql, Object... parameters) throws SQLException {
    return query(sql, new ArrayListHandler(), parameters);
}
7.4.6ArrayHandler

​ 单行数据存储到数组中,思路和ArrayListHandler,简化代码实现类如下:

实现类

public class ArrayHandler implements ResultSetHandler<Object[]> {
    @Override
    public Object[] handle(ResultSet res) throws SQLException {
        //实例化ArrayListHandler类处理结果集对象,只有单行数据
        List<Object[]> handle = new ArrayListHandler().handle(res);

        return handle != null ? handle.get(0) : null;
    }
}

​ 最终查询方法实现:

public Object[] queryArray(String sql, Object... parameters) throws SQLException {
    return query(sql, new ArrayHandler(), parameters);
}
8.总结

​ 当遇到重复的代码时,我们可以考虑封装,简化代码操作。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值