文章目录
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();
}
});
}
}
}
在static静态代码块中,静态代码块随着类的加载而加载,而且只加载一次。我们通过加载配置文件,获得了必要的数据库参数,同时加载了驱动。
获取数据库连接对象,通过DriverManager.getConnection(jdbcUrl, username, password)获得connection对象。
关闭连接资源对象,这里使用了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.总结
当遇到重复的代码时,我们可以考虑封装,简化代码操作。