基于JDBC的原生代码+反射机制,简单模拟DBUtils的实现方式
写在开头的话:
现代开发基于框架,mybatis、mybatis-plus才是常用的,DBUtils其实都很少用了,这篇笔记只是我当初刚了解JAVA与数据库的连接时,基于JAVA反射机制的一些钻研,认识的不算非常准确,仅供参考
问题描述
如果使用JDBC最原始的方式连接数据库,那么在执行查询的sql语句时,我们需要使用动态数组存放数据,比如下图的students对象,然后循环的将结果集中每一行数据与对象的成员变量对应,从而实现把结果集上的数据都存入对象中的目的
但是对比查询全表和查询单个数据的代码,我们发现这里的代码看起来重复性太高了,而且当SQL语句变化后,对应着这一赋值加取值的代码就得变化,代码复用性很低
左图是查询单个数据,右图是查询全表,将数据库表中的数据存入对象
如果我们可以在获取到RS结果集的同时,能确切的知道结果集的每一列的列名以及对应的数据类型,那么我们就可以通过循环的方式让JAVA自动根据结果集去调用Student的set函数去储存数据。
这样无论SQL语句如何变导致结果集如何变,我们只要把对象的成员变量定义好,就能实现自动存入数据,不用再去跟随SQL的变化而去修改存储数据的代码
这里面涉及两个难点:
①等式右边 由结果集给出列名、列的总数以及对应的数据类型
②等式左边 自动根据结果集所给数据调用作为容器的对象的对应set函数
第一个难点不是问题,毕竟结果集就存在rs对象中,而rs对象提供了许多方法让我们想查什么就查什么
注意rs对象提供的方法中获取列名的两种方法的区别:
带Label的会返回列名的别名,如果没有别名则返回本名
带Name的只会返回列的本名
ResultSetMetaData中getColumnLabel和getColumnName的区别 – freeTimeWY – 云海天 (cnblogs.com)
第二个难点则比较难解决
假想JAVA从数据库获得了结果集,要想自动的给JAVA一个容器,那么我们可以通过定义一个对象,然后使用动态数组储存对象,只要RS.next()为true,就表明还有新的“行”数据,此时利用循环,让结果集每一行数据都能对应一个新建的对象即可实现存储。
接下来问题就变成了一行有多列,如何把每一列的数据对应到对象的成员变量中,注意每一列的列名、数据类型不同:
其实问题就是如何让JAVA自动实现上图的代码
对于结果集,要根据不同的列相应的数据类型使用不同的get函数,例如getString() getData() getInt()
对于创建的对象,要根据不同的列名对应上对象中的成员变量,例如数据库中的id 对应对象中的private Integer id 此时就要用setId;数据库中的sname 对应对象中的private String name 此时就要用setSName;
那么问题就转变成
①我们要如何让JAVA能自动的根据RS结果集所给的列名反推出set函数名
②又如何自动的反推出set函数需要的参数类型
③接着又如何操作这个类创建出一个实例对象
④最后又如何自动去使用这个实例对象执行set函数去存储RS结果返回的数据
我们从头捋一遍思路:
①首先我们的目的很简单
在调用查询语句时要让JAVA自动的把结果集每一行数据存入我们声明的对象中,这些对象则组成集合,这样我们就能通过集合获取结果集每一行的数据
所以我们定义SQL语句、定义集合并声明集合元素为我们要储存的数据类型、使用我们写好的一个查询方法
查询方法相比传统 多了一个Student.class 这样就等于传入了一个指向Student的对象 JAVA就能使用这个对象自动操作这个类去创建student对象 并且调用对象的set函数存值
②进入我们写好的查询方法
首先整个方法使用了泛型 所以代码复用性更高了
接着我们看到了一个Class类对象 clazz 这就是我们外部传入的Student.class 所以clazz指向的是Student
③接着我们先处理第一个难点,让结果集返回我们需要的数据
基于RS.getMetaData() 我们能获得一个专门的对象 metaData用于获取结果集中的列的属性和数量
基于metaData.getColumnCount() 我们就能知道结果集有多少列 这就能决定要循环多少次 毕竟每一次循环就是将一列数据中的单个数据存入一个对象中
基于metaData.getColumnClassName(列的索引) 我们就能获取到结果集每一列的数据类型 是int 还是string 还是data
基于metaData.getColumnLabel(列的索引) 我们就能获取到结果集每一列的别名,如果没有别名则获得本名
最后RS基于列名 使用getObject方法 不去考虑获取到的数据的具体类型 就能实现逐列取出数据的目的
这里RS也能根据索引 i来取值 比较在for循环中i与列名一一对应 此处用列名还是i都行
④在搞定了等式右边后,我们回看等式左边
我们写了一个invoke方法 分别放入一个 “clazz调用自己指向的类的无参构造”创建的对象object 某一列的数据类型 某一列的列名 clazz 当前所在行列对应的值value
⑤我们进入invoke方法
在这里可以看到 我们做的第一个事情就是使用我们写的一个工具类,拼接set函数名
这样我们就获得了一个方法名 假如此时的列名是id 那么methodName就是setId 这个方法名可以理解为就是我们声明的类Student内部的那个setid()
接着我们又使用了一个工具类,获得一个指向某个类的class对象
这样我们就获得了第二个指向某个类的对象aClass 假如此时列名是id 对应数据类型为int 那么此时aClass就指向Integer类
然后我们使用了clazz.getMethod
基于这个方法 我们传入了set方法的全名 以及这个方法需要的参数 aClass ——>一个指向Integer类的Class类的对象 (为什么要传参?为什么 setId() 传入的参数是Integer类?)
这样就能获得clazz指向的类中与methodName同名的且声明的参数是Integer的方法 setId(Integer a)
最后使用Method类的对象独有的invoke方法 传入要执行method这个方法的对象object以及method这个方法需要的值value
以ID列为例,就等于令student.setId(value)
- 拼接set函数名:
- 获得一个指向某个类的class对象:
- 为什么要传参:
- 如果不给参数就等于去调用同名但无参的另一个方法 这属于方法重载下会产生的偏差 所以必须传参
具体到例子中就是获得了clazz指向的Student类中setId(传入一个Integer类参数) 方法
如果不传参 那就只能获得一个Student类中的无参的setId()方法 但这个方法可不存在 - 为什么 setId() 传入的参数是Integer类:
- 当然是因为我们基于面向对象的思维去声明对象时定义的 Student类中所有成员变量都是类
⑥保存结果 重新循环
在上面的操作后 以后我们每执行一次invoke方法 就等于自动让一个object对象执行了一个set函数 存入的是结果集中循环到的某个行列下的value值
然后将object对象存入List集合中 就实现了自动将结果集存入集合的操作
⑦最后我们回答一下上面整理的第二个难点的四个问题
解答①:
我们通过基于数据表设置了容器对象 要求对象中的成员变量的名字与数据表的列名一一对应 因此可以根据RS结果集所给的列名拼接出对应的set函数名
解答②:
我们知道如果希望获取类中的某个函数 例如这里的set函数 除了有函数名 还要求对应上参数表的数据类型 否则基于方法重载的机制,我们会获取到错误的方法
而我们又知道当我们set某个列的数时,对应的set函数的参数表就应该是这个列的数据类型
所以我们可以基于RS结果集所给的列的数据类型(给的是一个全类名) 对应到JAVA中的类(基于class.forName()) 而class.forName返回的就是set函数参数表需要的参数类型
然后已经有了指向某个类的对象clazz 以及具体的方法名、方法需要的参数表 那么获取到类中具体的某个方法自然就很简单了——基于getMethon()
这样我们就获得了需要的方法 并且以Method类的对象来保存这个方法
解答③:
我们拥有指向类的对象 clazz 那么直接调用clazz.newInstance() 即可调用无参构造获得一个对象object
解答④:
我们已经有了一个Method类的对象 那么基于这个对象的invoke方法 传入要执行这个方法的对象object以及值value
就实现了将RS结果集返回的数据存入相应对象的目的