消费贷管理持久层重构设计说明书Word文档下载推荐.docx
《消费贷管理持久层重构设计说明书Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《消费贷管理持久层重构设计说明书Word文档下载推荐.docx(15页珍藏版)》请在冰豆网上搜索。
![消费贷管理持久层重构设计说明书Word文档下载推荐.docx](https://file1.bdocx.com/fileroot1/2022-12/14/f504a936-e03f-47a0-b87a-614706161c24/f504a936-e03f-47a0-b87a-614706161c241.gif)
1.简介4
1.1.目的4
1.2.适用范围4
2.重构设计4
2.1.存在问题4
2.2.设计原则4
2.2.1.SQL分离原则4
2.2.2.简单高效原则5
2.3.设计方案5
2.3.1.动态映射方案5
2.3.2.单表面向对象存取方案6
2.3.3.SQL注入配置方案7
2.3.4.动态条件解决方案11
2.3.5.分页查询解决方案12
2.3.6.多表关联查询方案13
2.3.7.SQL批执行方案13
2.3.8.跨数据库解决方案13
1.
简介
1.1.目的
对系统数据访问层的设计理念、设计要点进行说明,以此作为开发和测试阶段的依据。
1.2.适用范围
项目组全体成员及公司消费贷项目设计和开发人员。
2.重构设计
2.1.存在问题
在数据库访问层面,我们总结了在项目开发过程中使用基础框架遇到的问题,并考虑客户一些建议,存在的问题我们大致归纳为下表:
序号
问题描述
1
不支持复杂的自定义SQL的执行,二次开发人员只得另行编写数据访问层,最终导致系统数据访问接口不统一、混乱,不利于问题排查和日后维护。
2
不支持使用普通的业务实体对象进行数据传输:
EMP中的TableModelDao只支持KeyedCollection和IndexedCollection这样动态存储结构的数据传输,虽然灵活,但是数据存取不明确,一旦有问题,不利于排查。
3
不支持批量执行SQL,导致项目开发人员在开发过程中编写了大量循环操作数据库的代码,对系统性能影响较大。
4
依赖配置文件实现ORM映射关系,这样导致系统配置文件过多,使系统文件的维护过于繁琐;
另外,一个ORM配置文件对应一个表的僵化映射关系,对多表关联查询的SQL支持乏力;
另外通过配置文件进行映射必定要用到JAVA的反射机制,这也是性能消耗的地方。
5
开发人员在JAVA代码中编写大量SQL并根据前端传入参数拼写WHERE子句的条件串,容易留下SQL注入攻击漏洞,为排查和修复这些漏洞以及应付甲方的安全扫描需要花费不少工作量;
另外,甲方希望SQL和JAVA代码严格分离。
2.2.设计原则
2.2.1.SQL分离原则
SQL分离原则的核心思想是:
期望二次开发人员在项目实施过程中,所有的SQL语句都必须从源代码中剥离出来;
数据访问层的所有解决方案需要遵循该原则,从架构层面提供保障,使二次开发人员在使用数据访问层时,无法把SQL语句和源代码耦合在一起。
2.2.2.简单高效原则
简单高效原则的核心思想是:
数据访问层的所有解决方案在设计实现后,都需要最大限度的保障二次开发人员在使用数据访问层开发时简单直观方便和高效。
2.3.设计方案
2.3.1.动态映射方案
2.3.1.1.方案目的
解决配置文件映射方案映射僵化和对多表关联查询支持乏力的问题;
解决ORM映射关系配置文件过多、维护繁琐的问题;
消除数据访问层的反射逻辑,提供其存取性能;
期望通过业务实体类明确的来传输数据,而不是类似KeyedCollection的模糊结构。
2.3.1.2.设计思路
该方案是基于业务实体类的一种动态ORM映射方案,该方案的关键在于业务实体类的内部结构,一是我们在业务实体类内置了一个Map结构用来存储业务实体类对应表字段值,而对外使用时,依然通过业务实体类相应属性的get和set方法进行存取,这样既保证了上层调用时的明确性,同时内置的Map结构保留了KeyedCollection结构灵活性的优点,为多表关联查询和动态映射提供了很好的支持。
二是在业务实体类构造函数中,初始化了其对应的物理表名和主键字段名,这样为单表的面向对象存取提供了保障。
2.3.1.3.设计实现
首先,采用该动态映射方案的业务对象类,需要继承抽象类CMISDomain,该抽象类的结构见如下图表说明:
对于该类图2-3-1,其具体含义参见下表说明:
属性或方法
中文含义
说明
属性
dataPool
表字段容器
Map结构,用来存放物理表字段值,是动态映射关系的基础
tableName
物理表名
字符串类型,用来存放物理表名
primaryKey
主键字段名数组
字符串数据,用来存放物理表一个或多个主键字段名称
方法
putData
设置表字段容器方法
getDataMap
获取表字段容器方法
6
getTableName
获取物理表名方法
7
getPrimaryKey
获取主键字段方法
实现案例
通过上述实体类的结构,我们把业务对象类的明确性和Map结构的灵活性较好地结合在一起,完成了ORM动态映射,该结构是动态映射的基础。
2.3.1.4.优缺点
相对之前的配置文件映射方案,该方案映射关系动态灵活直观,无需考虑数据库表字段和业务实体对象属性名称的不一致性,方便开发人员维护,也避免了反射的使用;
同时为单表面向对象存取提供了保障,对多表关联查询提供了支持。
不足之处,业务实体类需要继承特定的抽象类CMISDomain;
此外,业务实体类贯穿整体架构各个逻辑层面,职责多元化。
2.3.2.单表面向对象存取方案
2.3.2.1.方案目的
解决单表面向对象方式存取的问题。
所谓单表是指一个业务功能或业务对象的存取只对应数据库中某一个特定物理表。
2.3.2.2.设计思路
因为基于单表存取的业务功能的增删改查,逻辑规范、简单且重复,易于抽象,而让二次开发人员去编写这些简单重复的代码逻辑,有悖于简单高效原则。
在架构层面为单表存取提供一种简洁高效的方案成为必要,而前述动态映射方案的引入,为单表面向对象存取提供了结构基础和实现可能。
实现思路很简单:
只须把继承了抽象类CMISDomain的特定业务实体类实例传入到数据访问层,之后数据访问层核心类SqlClient会根据业务实体类的映射结构和实例值解析出相应的可执行SQL并完成数据库存取。
2.3.2.3.设计实现
设计实现上,在动态映射结构的基础上,SqlClient类针对增删改查操作分别提供了一个API,四种API如下:
publicstaticObjectqueryAuto(CMISDomaindomain,Connectionconn)throwsSQLException
publicstaticintdeleteAuto(CMISDomaindomain,Connectionconn)throwsSQLException
publicstaticintupdateAuto(CMISDomaindomain,Connectionconn)throwsSQLException
以上四个API方法完成了对前述设计思路的支持;
SqlClient类中的API按SQL的解析来源可分为两大类,前述四个API代表了一类,另一类API是基于配置文件的,在后续SQL注入配置方案中会阐述。
2.3.2.4.优缺点
该方案从上层调用来看是采用对象方式操作,而内部存取则根据对象实例自动组装SQL来完成,既保证了外部调用代码的优雅和简洁,又省去了配置SQL的时间和维护成本,使开发效率变得更高。
2.3.3.SQL注入配置方案
2.3.3.1.方案目的
解决SQL注入带来的安全漏洞问题。
2.3.3.2.设计思路
该方案类似iBatis,要求把SQL从JAVA逻辑中剥离出来在相应的XML文件中进行配置,然后应用系统在启动时预先装载SQL上下文环境中;
在真正使用时,数据访问层核心类会根据传入的SQL标识,到SQL上下文环境定位到要执行的配置SQL串,并用“?
”替换配置SQL串中的变量使之成为可执行的SQL语句,最后交给JDBC的PreparedStatement对象执行。
2.3.3.3.设计实现
首先,是SQL配置文件的基本结构,一条简单的查询SQL配置格式如下:
<
SQLid="
tests01"
resultClass="
com.yucheng.cmis.platform.organization.domain.SDuty"
onlyReturnFirst="
false"
>
<
SELECT>
!
[CDATA[
select*froms_duty
whereorderno=${orderno}anddepno=${depno}
]]>
/SELECT>
/SQL>
其中,id标识的值为配置SQL的标识,数据访问层核心类会根据该SQL标识去定位
SQL;
resultClass属性指明了开发人员期望查询返回的数据类型;
SELECT节点的文本部分配置了要执行的SQL串,该SQL串是类SQL语句,其中参数值是用${变量名}格式来描述的,数据访问层核心类SqlClient会根据变量名用”?
”号替换该格式,同时会根据变量名来获取变量值,并把变量值按变量配置的顺序设置到JDBC的PreparedStatement对象中。
对于SQL配置文件的全文结构,可以参考如下文档:
以上是配置文件的基本结构说明,是SQL注入配置方案的基础,具体的实现逻辑是通
过以下类来协调完成的,类图如下:
其中SqlConfig是用来缓存SQL配置文件内容的,这是在系统启动时由相关装载类来
完成,此处不详细说明。
SqlParser:
配置文件内部解析类,是根据SqlConfig提供的配置信息进行解析,并提供解析后的可执行SQL和参数值列表,该类也提供原生的配置SQL获取方法。
SqlConfigContext:
SQL配置上下文环境类,该类是SQL配置信息对外的接口类,所有SQL配置信息都通过该类获取。
最后是数据访问层核心类SqlClient,SqlClient的API根据SQL语句的解析来源大
致可以分为两大类:
一类是单表面向对象存取方式的API,一类是根据SQL配置标识进行数据库存取的API,后者是对SQL注入配置方案的支持和实现,通常这类API需要传入三个最基本的参数:
参数名
sqlId
Sql配置标识
字符串类型,对应SQL配置文件中特定SQL的ID属性值,是标识配置SQL的唯一标识
parameter
参数集合
数据类型可以是Map、CMISDomain和KeyedCollection,无论前述哪种类型底层都是Map结构实现,Map中的参数值对应配置SQL中的”${变量名}”格式变量,其Map的键值需要和变量名保持一致。
如果数据类型是简单数据类型,则SQL配置中的变量名必须是约定的_SIG_VALUE
conn
数据库连接
具体的API案例,比如普通查询API,如下:
publicstaticObjectqueryFirst(StringsqlId,Objectparameter,Connectionconn)throwsSQLException
又如删除的API则如下:
publicstaticintdelete(StringsqlId,CMISDomainparameter,Connectionconn)throwsSQLException
当然,随着数据库存取场景不同,在一些API中还会增加其他扩展参数,这个在后续方
案将逐步说明。
在考虑SQL注入配置方案设计实现的过程中,遇到一些具体的特殊问题,我们也提供了
较好的解决办法:
●Where子句中IN条件变量替换问题
因为IN条件的值的个数是不定的,不同于普通条件变量的解析,在SqlParse类解析时,
对IN条件变量进行”?
”号替换必定要进行特殊处理。
那么我们的解决方案是:
首先在SQL配置文件IN条件变量的配置同普通条件变量配置的方法一样;
其次,因为IN条件的值个数不定,我们要求SqlClient中API对应的传入参数的类型是集合数据类型,约定可以是数组、LIST集合对象、Map容器三种(无论哪种,该参数都是和其他参数一样嵌套在前述第二个基本参数的Map结构中,是以前述第二个基本参数为载体进行传递的),然后底层的SqlParse类会根据集合的长度拆分和计算出IN值的个数,并用对应个数的”?
”替换配置的IN变量名。
●Where子句中LIKE条件变量替换问题
LIKE条件因为是模糊查询,其后的变量值根据模糊查询的场景,需要追加“%”前缀或
后缀或者变量值前后都需要追加“%”符号,而在给JDBC的PreparedStatement对象set参数值时,程序是无法判定参数的逻辑运算符号是何种类型的,即无法判定参数的逻辑运算符号是”=”号,还是”>
”,抑或是”LIKE”运算符,因为没有提供相关的参数类型标识。
那么追加”%”的时机,我们没有在数据访问层解决,而是前移到OP层和页面去协作解决。
首先,对于页面中用来模糊查询的Input元素,我们要求其命名必须加上特定前缀,这个前缀是可注入配置的,缺省是”like”、”leftlike”、”rightlike”,分别对应模糊查询的三种场景;
之后,Op层核心类CMISOperation会根据前缀的类型,为页面传入的参数值在适当的位置追加相应的“%”;
最后,在经过前述两步处理后,数据访问层对于LIKE条件变量只须象普通条件变量一样处理即可。
2.3.3.4.优缺点
通过SQL注入配置方案,开发人员失去了编写带有SQL注入攻击漏洞代码的机会,对
这类问题进行了提前预防,节省了所有项目中这类安全扫描问题的处理成本;
同时SQL和JAVA代码的分离,使JAVA代码更加简洁和易维护;
SQL的外置可配置,使跨数据库平台变得方便,只须预先根据数据库类型编写特定的SQL配置文件,然后使用时,根据项目实际的数据库类型,选择不同版本的SQL配置文件部署即可;
最后,该方案和动态映射方案有机结合在一起,对复杂的多表关联查询和动态映射进行了很好的支持。
2.3.3.5.备选说明
要实现SQL注入配置,iBatis其实是一个可选的、现成的成熟方案,但因为iBatis的SQL配置方式和配置语句相对复杂,且相应的辅助JAVA类过于冗长繁琐,和数据库访问层整体解决方案的契合度不高,所以没有选择。
2.3.4.动态条件解决方案
2.3.4.1.方案目的
解决SQL注入配置方案中的动态条件配置和实现问题。
所谓动态条件,即需要在程序运行时期,根据参数值来判定是否要动态加入到SQL中的WHERE子句的条件。
2.3.4.2.设计思路
因为动态条件在开发阶段是不确定的,且动态条件的个数也是不确定,是运行时期动
态加入的,所以不能直接配置在主SQL中,需要单列出来配置,并为每个动态条件命名唯
一的ID值;
之后要求二次开发人员,在组件层或者叫业务层,根据OP层传入的参数情况,
编码选择采用哪些动态条件的ID,组装后提交给数据访问层去解析和执行。
2.3.4.3.设计实现
首先,为了支持动态条件可以在XML配置文件单例配置这一思路,我们追加了动态
条件的配置格式,参考如下:
test3"
parameterClass="
com.ecc.emp.data.KeyedCollection"
com.yucheng.cmis.platform.organization.domain.SUser"
onlyReturnFirst="
selectactorno,actorname,birthday,telnumfroms_user
where1=1${condi1}${condi2}
OPT_CONDITIONid="
condi1"
relationType="
AND"
state=${state}
/OPT_CONDITION>
condi2"
telnumlike${telnum}
我们对比之前的SQL配置可以看到,在<
节点之后,此处增加了多个<
OPT_CONDITION>
节点,每个<
节点配置了一个动态条件,并命名了一个唯一的ID值,relationType属性配置了动态条件和其他条件的关系模式。
以上是配置文件格式上的支持实现;
同时,为了支持动态条件逻辑的实现,我们重载了数据访问层核心类SqlClient的API,并扩充了一个API方法参数,参考示例如下:
publicstaticCollectionqueryList(StringsqlId,Objectparameter,String[]conditionId,Connectionconn)throwsSQLException
其中参数conditionId,是扩充的参数,类型是字符串数组;
该参数用来接收配置的动态条件的ID数组(如果不是动态条件的ID数组,数据访问层是会忽略不处理),至于ID数组的组装,我们约定是由二次开发人员在组件层根据前端参数传入情况来选择配置的动态条件的ID标识完成;
也许你会说,为什么不直接把动态条件拼装成SQL片段传入到数据访问层,那是因为这样做违背了我们设计的一个原则:
SQL分离原则,所以只传动态条件的ID数组。
2.3.4.4.设计深入
前几个小节的阐述,虽然解决了动态条件的问题,但是我们在编码实现过程中发现:
二次开发人员为了组装动态条件的ID数组,需要编写繁琐的条件分支逻辑代码,动态条件越多,条件分支逻辑越多。
基于简单高效原则,我们需要更简洁的代码,所以对于动态条件的问题,我们提供了另一种选择:
即在SqlParse类解析动态条件的逻辑中,除了优先按动态条件的ID来匹配之外,我们增加了按动态条件配置的所有变量名来匹配的规则,当传入的参数容器中包含了动态条件对应的所有变量,则把该动态条件自动组装到配置的主SQL中去。
这样,二次开发人员在组件层只须直接传递参数到数据访问层即可,无须再编写条件判定逻辑。
但是为什么要两种匹配规则并行呢?
那是因为按变量名匹配的规则,本身也有缺陷,因为动态条件中可能不含变量,因为动态条件A中可能包含了动态条件B的所有变量,而按变量名取匹配的规则在前述场景中则无所适从,所以两种匹配规则的妥协和结合使用,是一种取舍,是为了更大限度地保证代码的简洁高效并且逻辑正确。
2.3.5.分页查询解决方案
2.3.5.1.方案目的
解决SQL注入配置方案中的分页查询问题。
2.3.5.2.设计思路
分页查询SELECT语句相对于一般的查询语句要复杂,且因数据库类型不同,分页查询SELECT语句写法不一;
此外,为了统计分页查询的总记录数,需要额外配置一条统计记录总数的SQL,这些都增加了编码的复杂度,所以简化分页查询的开发过程成为必要。
设计思路大致如下:
首先在属性文件中增加数据库类型参数来设置数据库类型;
然后运用工厂模式,根据数据库类型参数,获取对应相应数据库的SQL生成器类,由SQL生成器类生成适应相应数据库的分页查询语句;
最后把分页查询语句交给SqlClient类去执行。
2.3.5.3.设计实现
设计实现上,首先我们在系统属性文件cmis.properties增加参数DatabaseType,来标识项目或产品使用的数据库类型(缺省为Oracle),如下:
#-------------------------服务器相关配置------------------
DatabaseType=Oracle
之后,我们创建SQL生成器相关类,其类图关系如下:
其中SqlBuilder是SQL生成器接口,它定义了产生了分页查询SQL语句和统计记录总数SQL语句的两个方法,CommonSqlBuilder是抽象类,以Oracle为缺省类型,实现了接口SqlBuilder定义的两个方法,而MySqlBuilder、Db2SqlBuilder、SqlServerBuilder、OracleBuilder是CommonSqlBuilder的子类,分别根据其对应数据库类型,覆盖实现了基类的方法。
最后,SqlBuilderFactroy是工厂类,它会根据前述数据库类型参数,获取相应的SQL生成器类以生成相应数据库的分页查询语句及总记录数统计SQL语句。
最后,数据访问层核心类SqlClient需要提供API支持。
因为分页查询,所以通常都会需要两个额外的参数:
当前页号和每页记录数;
相关的API如下:
publicstaticintqueryCount(StringsqlId,Objectparameter,Connectionconn)throwsSQLException
publicstaticCollectionqueryList(StringsqlId,Objectparameter,intpageNumber,intpageSize,Connectionconn)throwsSQLException
其中,queryList()方式是用来执行分页查询的,queryCount()是用来统计总记录数的,它们内部都会调用前述SqlBuilderFactroy工厂类来获取相应的SQL语句以完成分页查询。
当然,关于分页查询,在类SqlClient中还有其他重载方法,此处不一一列示,请参看相关操作手册。
2.3.5.4.优缺点
该方案使二次开发人员在编写分页查询逻辑时,无需再关心分页查询如何编写以及数据库的差异性,只须象普通的查询SQL一样编写即可,而且也不再需要额外编写和配置统计总记录数的SQL语句,降低了开发难度和复杂度,一定程度上提高了开发效率。
2.3.6.SQL批执行方案
2.3.6.1.方案目的