JDBC高级¶
约 2625 个字 330 行代码 预计阅读时间 13 分钟
JDBC工具类封装(版本1)¶
前面每一次使用JDBC时都需要进行以下重复的操作:
- 创建连接池
- 获取连接
- 连接的回收
这些重复的操作就会在一个项目中出现代码的冗余,为了解决这个问题,可以对部分代码进行封装从而实现代码复用
为了保证工具类的正常使用,所有成员方法必须是静态的
因为创建连接池的步骤是先于其他操作的,所以可以考虑使用静态代码块包裹,但是为了保证其他方法可以访问到创建的连接池,需要将连接池对象引用作为静态成员属性
获取连接和连接的回收只需要简单封装对应的getConnection()
方法以及close()
方法即可
下面的代码以封装Druid连接池结合软编码为例:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Note
需要注意,静态代码块不能使用throws
向上层抛异常
ThreadLocal
类¶
JDK 1.2的版本中就提供java.lang.ThreadLocal
,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。通常用来在在多线程中管理共享数据库连接、Session
等。
ThreadLocal
用于保存某个线程共享变量,原因是在Java中,每一个线程对象中都有一个ThreadLocalMap<ThreadLocal, Object>
,其key
就是一个ThreadLocal
,而Object
即为该线程的共享变量
而这个返回的map
是通过ThreadLocal
的set
和get
方法操作的。对于同一个static ThreadLocal
,不同线程只能从中get
,set
,remove
自己的变量,而不会影响其他线程的变量。其作用如下:
- 在进行对象跨层传递的时候,使用
ThreadLocal
可以避免多次传递,打破层次间的约束 - 线程间数据隔离
- 进行事务操作,用于存储线程事务信息
- 数据库连接,
Session
会话管理
主要使用的方法:
- 方法:
ThreadLocal对象.get
:获取ThreadLocal
中当前线程共享变量的值 - 方法:
ThreadLocal对象.set
:设置ThreadLocal
中当前线程共享变量的值 - 方法:
ThreadLocal对象.remove
:移除ThreadLocal
中当前线程共享变量的值
结合ThreadLocal
类完善JDBC
工具类¶
第一个版本的JDBC工具类最大的问题就是如果一个用户在一定的时间内多次访问数据库的情况下,可能会出现多次从连接池获取对象再释放从而造成资源的浪费。例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
因为一个用户可以看作是一个线程,则可以控制该用户自己所在的线程内使用的是同一个链接对象,这里就需要使用一个ThreadLocal
来实现,而对应的ThreadLocal
中的类型就是Connection
,因为需要确保连接对象在同一个线程内唯一
Java | |
---|---|
1 2 |
|
确保连接对象在同一个线程内唯一,本质就是为了确保在获取连接对象时是同一个连接对象,所以此时就不可以直接调用getConnection()
方法获取,而是先从ThreadLocal
中获取,如果没有则通过getConnection()
方法获取,否则就直接返回ThreadLocal
中的Connection
对象。所以第一个版本的JDBC工具类中的获取连接方法可以修改为:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
对于释放连接,首先要做的就是从ThreadLocal
中移除已经存在的Connection
对象,这里就需要先进行一次获取,确保是某一个线程所拥有的Connection
对象,随后再释放该对象。因为一个线程内只使用同一个Connection
对象,所以此处只需要释放一次,从而也就不需要传递参数
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
对同样的代码进行测试:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
使用多线程进行测试:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
DAO封装介绍¶
DAO:Data Access Object,数据访问对象
Java是面向对象语言,数据在Java中通常以对象的形式存在。一张表对应一个实体类,一张表的操作对应一个DAO对象。在Java操作数据库时,一般会将对同一张表的增删改查操作统一维护起来,维护的这个类就是DAO层。DAO层只关注对数据库的操作,供业务层Service调用
封装一个简单的员工DAO层接口和实现类:
Note
本次封装只是简单的封装,并没有提供参数传递实参,其他思路基本一致
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
BaseDAO工具类设计¶
BaseDAO
工具类:基本上每一个数据表都应该有一个对应的DAO接口及其实现类,发现对所有表的操作(增、删、改、查)代码重复度很高,所以可以抽取公共代码,给这些DAO的实现类可以抽取一个公共的父类,复用增删改查的基本操作,称为BaseDAO
工具类
对于重复度很高的四个操作的代码主要可以分为两种:
- 查询:主要执行
executeQuery()
。 - 修改、删除和新增:主要执行
executeInsert()
作为工具类的BaseDao
类来说,可以考虑下面的设计思路:
-
修改、删除和新增:
- 参数1:因为是工具类方法,所以无法确定到底是「修改、删除和新增」中的哪一个,所以需要传递一个参数代表SQL语句,其类型需要与创建
preparedStatement()
方法参数类型一致 - 参数2:对于三种操作来说,都有可能涉及到具体的内容,所以需要使用在SQL中需要使用到占位符。为了保证本方法对三种操作都有效「修改一般一个占位符->一个内容、删除一般一个占位符->一个内容、新增一般多个占位符->多个内容」,可以考虑使用可变参数,可变参数的类型则为
Object
,确保对于不同的占位符对应的具体内容都可以正常接收 - 方法体:
- 获取连接对象
- 传入SQL语句
- 判断是否有占位符,如果有,根据可变参数个数填入可变参数数组中的数据;否则进行后面的步骤
- 调用
executeUpdate()
方法,并获取影响的行数 - 释放资源
- 返回对应的行数
示例代码:
Java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// 修改、新增和删除 public static int modify(String sql, Object...params) throws Exception{ // 获取连接对象 Connection connection = JDBCUtil.getConnection(); // 执行SQL语句 PreparedStatement preparedStatement = connection.prepareStatement(sql); // 判断是否有参数 for (int i = 0; params != null && i < params.length ; i++) { preparedStatement.setObject(i + 1, params[i]); } int i = preparedStatement.executeUpdate(); // 关闭连接 JDBCUtil.release(); preparedStatement.close(); return i; }
Note
需要注意:因为不确定SQL语句中每一个可用占位符对应的类型,所以此处只能使用
setObject()
方法,因为i
从0开始,而占位符从1开始,所以需要传入i + 1
,在函数返回影响的行数之前,需要进行资源释放防止资源泄漏 - 参数1:因为是工具类方法,所以无法确定到底是「修改、删除和新增」中的哪一个,所以需要传递一个参数代表SQL语句,其类型需要与创建
-
查询:查询存在三种情况:1. 单行单列 2. 单行多列 3. 多行多列。对于这三种情况来说,可以返回一个
List
集合:单行单列,集合中只有一个值(可以通过List
集合的getFirst()
方法获取,也可以通过get(0)
获取);单行多列,集合中只有一个对象(可以通过List
集合的getFirst()
方法获取,也可以通过get(0)
获取);多行多列,集合中有多个对象。考虑到将来可能查询的不只是一种类型(例如员工类、学生类或者老师类等),所以返回的List
集合对应的泛型位置不可以是具体的类型,此时需要用到泛型方法- 参数3:单行单列、单行多列和多行多列都有可能涉及到占位符,所以需要使用到可变参数
- 参数2:所有操作都涉及到SQL语句,所以需要传递对应的SQL语句
- 参数1:通过前面两个参数可以获取到一个结果集,需要遍历结果集将结果集中的数据存储到集合泛型对应的类的每一个字段中,但是会出现一个问题,因为无法确定具体是哪一个类型,也就无法直接确定该类中的字段(泛型没有具体字段)。直接调用行不通,那就反着来:使用反射,在传递参数时指示调用者需要传递具体类的类对象,在方法中就可以使用该类对象调用对应获取私有成员的方法间接获取到对应类的字段,再进行赋值,将结果存储到集合中
- 方法体:
- 获取连接对象
- 传入SQL语句
- 判断是否有占位符,如果有,根据可变参数个数填入可变参数数组中的数据;否则进行后面的步骤
- 执行SQL语句
- 实例化集合
- 获取结果集
- 获取结果的列数,作为工具类的方法,无法确定一共有多少个字段,所以需要使用结果集中的
getMetaData()
方法获取到一个ResultSetMetaData
对象(结果集元数据对象),该对象中有一个方法getColumnCount()
可以获取到结果集的列数,一个列代表一个字段,有了列数,也就可以确定字段的个数 - 遍历结果集,在遍历过程结果集中,通过反射创建一个对象,最好使用无参构造。接下来遍历每一列,这个过程中需要获取到每一行每一列对应的值,可以使用结果集的
getObject()
方法,因为无法确定具体字段,也就无法确定getXxx
方法的具体类型,使用getObject()
最合适。接着通过调用结果集元数据对象的getColumnLabel()
方法(不推荐使用getColumnName()
,因为这个方法只会取到列的真实名称,忽略别名)获取到列名(或者列的别名)。通过反射的getDeclaredFieldName()
方法,参数传递列名获取到一个字段,调用其set()
方法,将创建的对象和值传入方法中即可,如此往复直到结束一行数据,将对象存入到集合中。注意这一过程中,尽量执行「破私有为公有」 - 释放资源
- 返回
List
集合
示例代码:
Java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
// 查询 public static <T> List<T> select(Class<T> classType, String sql, Object...params) throws Exception{ Connection connection = JDBCUtil.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(sql); for (int i = 0; params != null && i < params.length; i++) { preparedStatement.setObject(i + 1, params[i]); } ArrayList<T> ts = new ArrayList<>(); ResultSet resultSet = preparedStatement.executeQuery(); // 获取结果集元数据 ResultSetMetaData metaData = resultSet.getMetaData(); // 获取列 int columnCount = metaData.getColumnCount(); while (resultSet.next()) { // 反射创建一个对象 T t = classType.newInstance(); // 遍历列 for (int i = 0; i < columnCount; i++) { Object object = resultSet.getObject(i + 1); // 获取列名 String columnLabel = metaData.getColumnLabel(i + 1); // 反射获取字段 Field declaredField = classType.getDeclaredField(columnLabel); declaredField.setAccessible(true); // 通过反射赋值 declaredField.set(t, object); } ts.add(t); } // 释放资源 resultSet.close(); preparedStatement.close(); JDBCUtil.release(); // 返回结果 return ts; }
DAO实现类与BaseDao结合(测试)¶
员工实体类:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
DAO实现类完善:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
Note
需要注意,在查询时,因为Employee
类字段名为小驼峰式,与数据库中的表头不对应,所以需要在查询时为表头取别名
测试方法:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|