掌桥专利:专业的专利平台
掌桥专利
首页

基于通用数据访问框架的数据查询方法

文献发布时间:2023-06-19 19:28:50


基于通用数据访问框架的数据查询方法

技术领域

本发明涉及数据访问技术领域,尤其涉及一种基于通用数据访问框架的数据查询方法。

背景技术

随着数据存储能力的提高,产生了各种不同的数据库,面对日益复杂的需求,企业也会采用多样化的存储形式。在支撑灵活的数据查询和分析工作中,就需要对不同来源的数据进行处理,如何快速开发并满足不同数据的查询需求是在开发系统时比较关注的问题。

对于关系型数据库,现有的开源框架封装了数据查询方法,便于开发人员查询数据和封装SQL(Structured Query Language,结构化查询语言)语句,比如Hibernate、JPA(Java Persistence API,Java持久层API)、Mybatis,对于复杂的业务数据获取,往往直接编写符合数据库语法的SQL进行查询。对于直接存储在内存的数据,或者内存数据库的数据,一般是先获取内存数据对应的对象,通过get方法获取具体的值,或者将内存数据放在集合中,通过操作集合的方法来处理数据。

在采用开源框架中面向对象的查询方式时,通常适用于简单的数据查询,而且开发人员需要根据查询的业务类型,构建出对应的业务对象;采用封装SQL或直接编写SQL时,不同类型的数据库,或者有些数据库不同版本,都存在SQL语法不同的问题。往往需要根据数据库类型,封装多套SQL来满足不同的语法要求,而且需要注意SQL注入、SQL拼接顺序、SQL动态参数与占位符的匹配等问题;如果需要对内存数据中多个对象数据进行关联、比对,或者内存中的对象数据与数据库中获取的数据,与文件中的数据交互匹配时,需要由开发人员根据具体的需求,获取所需的内存数据后,再与其它数据通过编写程序来处理。而且,如果项目中既要对数据库的数据进行处理,又要对内存数据处理,开发人员就需要采用多种开发方式,影响开发效率,定制开发的代码也无法复用,代码质量和查询方式都不可控。

发明内容

鉴于上述的分析,本发明实施例旨在提供一种基于通用数据访问框架的数据查询方法,用以解决现有对不同存储形式的数据的查询逻辑不统一且处理逻辑复杂的问题。

本发明实施例提供了一种基于通用数据访问框架的数据查询方法,包括如下步骤:

基于待查询数据的访问方式,获取数据源;

获取查询条件,调用查询构造器,通过链式调用方式对待查询数据构造查询对象语法树,基于查询对象语法树获取查询对象;

根据数据源,创建查询执行器;

将查询对象传入查询执行器中,执行查询方法后得到查询结果。

基于上述方法的进一步改进,基于待查询数据的访问方式,获取数据源,包括:

当待查询数据存储于数据库中,且项目框架未采用Hibernate或JPA,则通过数据库表名和字段名访问待查询数据,获取JDBC数据源;

当待查询数据存储于数据库中,且项目框架采用了Hibernate或JPA,则将待查询数据封装为实体对象,获取JPA数据源;

当待查询数据存储于内存中,则将待查询数据封装为内存表对象,获取内存数据源;

当待查询数据存储于常量数据集,则将待查询数据封装为内存表对象或常量查询引用对象,获取内存数据源。

基于上述方法的进一步改进,内存表对象包括:表别名、数据列表和字段访问器;

数据列表包括属性名和属性值;

字段访问器用于从数据列表中指定需要提取的1个或多个属性名;

常量查询引用对象实现了根引用接口和数据集接口;

数据集接口包括获取数据集表头和获取数据列表的方法,常量查询引用对象在获取数据列表的方法中采用二维数组格式存储数据;在获取数据集表头方法中将数据列表的属性名设置为表头。

基于上述方法的进一步改进,查询对象语法树上的各个节点是实现了相同的父引用接口的引用对象,父引用接口包括一个获取元素类型的方法,引用对象在获取元素类型的方法中定义对应的SQL语法元素类型;

根引用接口是父引用接口派生的一个子引用接口;

查询条件封装为SQL语法元素类型为常量的引用对象,作为查询对象语法树上的节点。

基于上述方法的进一步改进,查询构造器包括多个与SQL组成部分对应的接口,在每个接口实现类中通过start和end开头的方法表示构造SQL组成部分的开始和结束;

构造查询对象语法树,包括:通过链式调用方式,调用多组start和end开头的方法,在每组start和end开头的方法中挂接1个或多个引用对象;一棵查询对象语法树描述一条完整的SQL语句。

基于上述方法的进一步改进,基于查询对象语法树获取查询对象,包括:

将语法树上具有相同元素类型的引用对象放在对应的引用对象集合中;

将引用对象集合放入语法树根节点的根引用对象中,返回根引用对象;

根引用对象是查询对象语法树根节点对应的实现了根引用接口的引用对象。

基于上述方法的进一步改进,根据数据源,创建查询执行器,包括:

如果是JDBC数据源,调用执行配置器获取JDBC执行器,并创建查询转译器,以及根据JDBC数据源获取目标数据库类型,将与目标数据库类型对应的写入器注册至查询转译器中;

如果是JPA数据源,调用执行配置器获取JPA执行器,并创建查询转译器,将JPA写入器注册至查询转译器中;

如果是内存数据源,直接获取内存执行器。

基于上述方法的进一步改进,写入器具有相同的父写入器接口,用于执行以下操作:获取写入器处理类型、准备转译和写入转译;其中,

获取写入器处理类型,用于定义写入器可处理的元素类型,且与一个引用对象中定义的元素类型对应;

准备转译,用于根据目标数据库类型,使用等价语法转换查询对象中的引用对象,调整引用对象之间的节点挂接关系,得到处理后的查询对象;

写入转译,用于根据处理后的查询对象中引用对象对应的元素类型,在上下文的字符串建造者中生成对应目标数据库类型的SQL片段或对应JPA的HQL片段,和/或在参数列表缓冲区中写入值。

基于上述方法的进一步改进,将查询对象传入查询执行器中,执行查询方法后得到查询结果,包括:

当调用JDBC执行器或JPA执行器执行查询时,以查询对象的根引用对象对应的写入器作为查询转译器调用的入口写入器,在准备转译和写入转译中,根据其中定义的遍历顺序,分别对查询对象语法树进行遍历,对处理后的查询对象合并各写入器上下文中的字符串建造者和参数列表缓冲区,得到完整的SQL语句或HQL语句,以及参数列表,执行后得到查询结果;

当调用内存执行器执行查询时,获取查询对象的各引用对象集合,先获取表引用对象集合,对表引用对象排序、合并,得到宽表集合,再根据布尔条件引用对象集合对宽表集合进行筛选,接着根据排序引用对象集合对宽表集合进行排序,最后根据分组引用对象集合对宽表集合进行分组,得到查询结果。

基于上述方法的进一步改进,查询结果封装为实现了数据集接口的数据集对象,或者,封装为常量查询引用对象,用于内存数据查询中。

与现有技术相比,本发明至少可实现如下有益效果之一:

1、按照统一的SQL编写思维,使用链式方法调用来构造查询对象语法树,无需考虑SQL拼接顺序,并支持多层次子查询嵌套,支持复杂SQL的构造,降低了构造SQL的难度;

2、只需构造一个查询对象,提供一套查询逻辑就可支持不同类型、不同版本的数据库、内存数据和常量数据集,提高了代码的复用度,数据库的兼容性,降低了维护成本;

3、通过查询转译器集中处理不同数据库特殊转换,避免使用者编写不规范的原生SQL,避免SQL语法错误,提高了代码质量,降低了出现性能问题的风险;

4、通过扩展自定义的函数、引用对象、写入器、执行器和数据集,满足个性化的使用;

本发明中,上述各技术方案之间还可以相互组合,以实现更多的优选组合方案。本发明的其他特征和优点将在随后的说明书中阐述,并且,部分优点可从说明书中变得显而易见,或者通过实施本发明而了解。本发明的目的和其他优点可通过说明书以及附图中所特别指出的内容中来实现和获得。

附图说明

附图仅用于示出具体实施例的目的,而并不认为是对本发明的限制,在整个附图中,相同的参考符号表示相同的部件。

图1为本发明实施例中基于通用数据访问框架的数据查询方法流程图。

具体实施方式

下面结合附图来具体描述本发明的优选实施例,其中,附图构成本申请一部分,并与本发明的实施例一起用于阐释本发明的原理,并非用于限定本发明的范围。

本发明的一个具体实施例,公开了一种基于通用数据访问框架的数据查询方法,如图1所示,包括如下步骤:

S11:基于待查询数据的访问方式,获取数据源;

需要说明的是,通用访问框架作为工具Jar集成到开发项目中,其中提供了不同的数据源,包括JDBC数据源、JPA数据源和内存数据源,用于支持不同存储形式的数据查询。

待查询数据指业务系统中所有涉及查询的数据,根据待查询数据的存储形式和项目框架,确定待查询数据的访问方式,从而获取对应的数据源。

具体来说,当待查询数据存储于数据库中,且项目框架未采用Hibernate或JPA,则通过数据库表名和字段名访问待查询数据,获取JDBC数据源,即通过数据库驱动建立与数据库的连接;

当待查询数据存储于数据库中,且项目框架采用了Hibernate或JPA,则将待查询数据封装为实体对象,获取JPA数据源,即通过JPA配置数据库的连接;

需要说明的是,这种情况也可以获取JDBC数据源,但是建议获取JPA数据源,可以避免直接读写数据库导致hibernate缓存和数据库不一致的问题。

当待查询数据存储于内存中,则将待查询数据封装为内存表对象,获取内存数据源;

当待查询数据存储于常量数据集,则将待查询数据封装为内存表对象或常量查询引用对象,获取内存数据源。

需要说明的是,内存数据有多种来源,比如来源于业务系统的内存中,来源于内存数据库,而对内存数据的处理,又不仅仅只是这些内存数据,还可以关联文件中的数据,关联数据库查询得到的结果集,将这些数据封装为内存表对象,就可以在查询或关联查询这些数据时,通过内存数据源以虚拟的物理表形式访问内存表对象。

具体来说,内存表对象包括:表别名、数据列表和字段访问器,如果表别名与内存数据源中的已存在的表别名相同,将会进行覆盖。其中,数据列表包括属性名和属性值,字段访问器用于从数据列表中指定需要提取的1个或多个属性名。当数据列表中有很多属性数据,而只需要对部分属性数据进行访问时,通过字段访问器就可以直接选择所需要的数据放入内存数据源中,无需人工处理。

对于部分常量数据集不想放入内存数据源,但是需要在数据查询中使用,则可以封装为常量查询引用对象,在后续查询对象的构造时,将常量查询引用对象的对象名作为虚拟表名使用。

S12:获取查询条件,调用查询构造器,通过链式调用方式对待查询数据构造查询对象语法树,基于查询对象语法树获取查询对象;

需要说明的是,查询构造器是按照编写SQL的思路来构造,考虑到一个SQL语句的组成部分包括:选择列名select,查询表名from,过滤条件where,分组字段group by,排序条件orderby,分组条件having,表合并union all/union,因此,查询构造器中组合了很多接口,包括Select、From、Where、Group、Order、Having和Merge,分别对应SQL的各个组成部分,每个接口实现类中通过start和end开头的方法表示构造SQL组成部分的开始和结束。

具体来说,通用访问框架中查询构造器提供了以下方法:

startSelect,用于设置选择列名;endSelect,用于表示当前选择列名设置完成;

startFrom,用于设置查询的表;endForm,用于表示当前查询的表设置完成;

startWhere,用于设置过滤条件;endWhere,用于表示当前过滤条件设置完成;

startGroup,用于设置分组字段;endGroup,用于表示当前分组字段设置完成;

startOrder,用于设置排序字段和排序顺序;endOrder,用于表示当前排序字段和排序顺序设置完成;

startHaving,用于设置分组条件;endHaving,用于表示当前分组条件设置完成;

startMerge,用于设置表合并;endMerge,用于表示当前合并设置完成;

page,用于设置查询的分页条件;

limit,用于设置查询结果的最大条数;

需要注意的是,page与limit方法互斥,其它方法调用时不分先后顺序,根据SQL语法组合调用上述方法,在构造完成后调用build方法创建和生成查询对象;

需要说明的是,可以用一条语句或多条语句来构造一棵语法树,一棵语法树描述一条完整的SQL语句。在一条语句中组合调用多个方法构造多个SQL组成部分时,各start和end开头的方法成对调用,如果通过多条语句,每条语句只构造SQL的一个组成部分时,可以只调用start开头的方法,代码换行相当于调用了end开头的方法。当逻辑十分复杂时,这种可自由构造SQL的方式使得开发人员可以将注意力集中在业务上,而不需要操心SQL的各种组成部分拼接时的先后关系、括号是否匹配、是否存在SQL注入攻击、语法是否符合目标数据库等问题。

示例性地,需要查询客户ID大于1的客户名称,如果用SQL语句,表示为“SELECTC.CUSTOMERNAME AS客户名称FROM CUSTOMER C WHERE C.ID>1”,如果利用查询构造器,则通过HQueryFacade.createQueryBuilder()方法创建一个查询构造器,可以用一条语句将上述SQL语句构造成一棵语法树,示例代码如下:

HQuery query=

HQueryFacade.createQueryBuilder().startFrom().tab(“CUSTOMER”,“C”).endFrom().startSelect().col(0,“CUSTOMERNAME”,“客户名称”).endSelect().startWhere().gtr(getCol(0,“ID”),getVal(1)).endWhere().build();

也可以用多条语句将上述SQL语句构造成一棵语法树,示例代码如下:

HQueryBuilder querybuilder=HQueryFacade.createQueryBuilder().startFrom().tab(“CUSTOMER”,“C”).endFrom();

querybuilder.startSelect().col(0,“CUSTOMERNAME”,“客户名称”);

querybuilder.startWhere().gtr(getCol(0,“ID”),getVal(1));

HQuery query=querybuilder.build();

查询构造器构造SQL的组成部分时,每个组成部分的数据被封装为引用对象,作为语法树上的一个节点,被放在各组成部分对应的引用对象集合中,语法树的根节点也有对应的引用对象HQuery,包含各个子节点的引用对象集合。

优选地,引用对象集合采用ArrayList类型。

引用对象是用SQL语法元素描述SQL组成部分,包括表引用对象、选择列引用对象、分组引用对象、排序引用对象、布尔条件引用对象、字段引用对象、常量引用对象和函数调用引用对象等,这些引用对象实现了相同的父引用接口HValRef,均包括一个获取元素类型的方法,用于定义引用对象对应的SQL语法元素类型。

在上述示例代码中,通过tab(“CUSTOMER”,“C”)方法,构造了一个表引用对象,其中“CUSTOMER”是表引用对象中的表名,“C”是表引用对象中的表别名,该表引用对象会放入表引用对象集合中;通过col(0,“CUSTOMERNAME”,“客户名称”)方法,构造了一个选择列引用对象,其中“0”是列所属的表索引,即第1张表,“CUSTOMERNAME”是列名,“客户名称”是列别名,该选择列引用对象会放入选择列引用对象集合selList中;通过gtr(getCol(0,“ID”),getVal(1))构造了一个布尔条件引用对象,其中gtr是布尔条件的运算符,表示“>”,getCol(0,“ID”)是布尔条件的左边,表示被比对字段是第1张表的“ID”字段,getVal(1)是布尔条件的右边,表示比对值是“1”,该布尔条件引用对象会放入布尔条件引用对象集合boolList中。

优选地,考虑到使用between运算符时会有2个比对值,布尔条件引用对象支持2个右值。

需要说明的是,父引用接口派生出了很多子引用接口,每个引用对象直接实现的是对应的子引用接口,在继承父引用接口中方法的同时可以增加新的方法。以布尔条件引用对象为例,构造引用对象并放入对应的引用对象集合的示例代码如下,其中HBoolExpImpl是布尔条件引用接口HBoolRef的实现类,HBoolRef是父引用接口HValRef派生的一个子引用接口:

//构造布尔条件引用对象

HBoolRefbool=

new HBoolExpImpl(getCol(0,“ID”),GTR,getVal(1),null);

//将布尔条件引用对象放入布尔条件引用对象集合中

ListboolList=Arrays.asList(bool);

引用对象对应SQL语法元素,包括:列SEL、字段COL、表FRM、分组GRP和常量CST等,需要说明的是,元素类型支持自定义,比如可以定义“CST_USER”为扩展的常量引用,与通用的CST区分对不同类型或不同版本的数据库的不同处理方式。对于内存数据的查询,如果涉及到复杂函数的处理,可以扩展新的元素类型。

对于查询时传入的查询条件,以变量名的形式直接用于查询对象语法树中,被封装为SQL语法元素类型为常量的引用对象,作为查询对象语法树上的节点。

值得注意的是,现有技术中,当SQL语句中需要动态传入查询条件时,为了避免SQL注入攻击,而用JDBC占位符“?”来表示SQL中的动态参数,但是当查询逻辑比较复杂时,开发人员在构造SQL时很难将占位符和对应的参数值一一匹配,出现问题时也很难排查。虽然Hibernate中可以将变量和参数值存放在Map中,但是当变量比较多,或者同一个变量要在SQL中使用多次时,就要使用“变量名+序号”的方式来防止变量名重复,出现问题时也很难排查。而本实施例中,查询构造器在各组成部分中通过挂接引用对象来构造语法树,对于动态参数可以作为语法树上的一个节点,用实现了HValRef接口的常量引用对象来定义,在构造时无需考虑占位符问题。

对于未放入内存数据源中的常量数据集对应的是常量查询引用对象,可以作为子查询引用对象的子查询数据集,常量查询引用对象同时实现了根引用接口和数据集接口。其中,根引用接口是父引用接口派生的一个子引用接口,是根引用对象实现的接口,实现该接口使常量查询引用对象可以挂接在查询对象语法树上;数据集接口包括获取数据集表头和获取数据列表的方法,常量查询引用对象在获取数据集表头方法中将数据列表的属性名设置为表头,在获取数据列表的方法中采用二维数组格式存储数据列表。

具体来说,数据集对象DataSet接口继承了List接口,增加了两个方法,分别用来获取查询结果集、获取查询结果的元数据;其中查询结果集采用二维数组格式List存储。这种结构方便对数据结果进行管理,比如对数据结果的列进行重命名时,只需要修改查询结果的元数据即可,大大提升了修改效率,而使用二维数组存储查询结果集可以大大减少存储所需的空间。

语法树上可以挂接任意实现了父引用接口的引用对象,每个引用对象可以再挂接其它引用对象,也可以挂接子语法树,从而实现语法树的无限拓展,可以有效降低SQL的构造难度。

示例性地,待查询字段是另一个表中的一个字段,示例代码如下:

querybuilder.startSelect().val(HQueryFacade.createQueryBuilder().startSelect().col(0,“TOTALS”,“TOTALS”).endSelect().startFrom().tab(“PRODUCT”,“P”).endFrom().build(),“销售量”);

考虑到查询时,通常都是多表关联,包括leftjoin、rightjoin、innerjoin和fulljoin,就需要将引用对象派生出子引用对象来实现。优选地,将表引用对象HFrmRef,又派生出主表名HFrmNRef,用于表示直接根据表名进行关联;子查询引用对象HFrmQRef,用于表示将一个子查询结果作为表进行关联;表关联引用对象HFrmJRef,用于定义关联的左表表名、关联方式和关联条件。而表关联时既可以直接关联表名,又可以关联查询子表,因此基于上述三个派生的子接口,又派生出关联表名引用对象HFrmJNRef,用于表示关联的右表是直接的表名;关联子表引用对象HFrmJQRef,用于表示关联的右表是子查询。

示例性地,A表与B表通过A表的AID与B表的BID左外连接,在构造语法树时的示例代码如下:

querybuilder.startFrom().tab(“A”,“A表”).join(0,“AID”,JoinType.LEFT_JOIN,“B”,“B表”,“BID”).endFrom();

代码中通过tab(“A”,“A表”)方法,构造了一个主表名引用对象HFrmNRef,通过join(0,“AID”,JoinType.LEFT_JOIN,“B”,“B表”,“BID”)方法,构造了一个关联表名引用对象HFrmJNRef,其中包含表引用对象HFrmRef、关联方式JoinType和布尔条件引用对象HBoolRef,分别用于定义关联的左表的引用对象、关联关系和关联条件。

基于构造完成的查询对象语法树获取查询对象,是将语法树上具有相同元素类型的引用对象放在对应的引用对象集合中,将引用对象集合放入语法树根节点的根引用对象中,返回根引用对象。

S13:根据数据源,创建查询执行器;

具体来说,根据步骤S11获取的数据源,创建对应的查询执行器,查询执行器也是一个抽象接口,可以根据查询对象中的对象类型进行扩展,通用数据访问框架中的查询执行器包括:

如果是JDBC数据源,调用执行配置器获取JDBC执行器,并创建查询转译器,以及根据JDBC数据源获取目标数据库类型,将与目标数据库类型对应的写入器注册至查询转译器中;

如果是JPA数据源,调用执行配置器获取JPA执行器,并创建查询转译器,将JPA写入器注册至查询转译器中;

如果是内存数据源,直接获取内存执行器。

需要说明的是,在查询构造器构造查询对象时,不需要考虑不同数据库类型、相同数据库不同版本之间的差异,通过相同的引用对象来构造查询对象,提高了使用的便利性和开发效率。在执行查询时,根据数据库类型和版本再由不同的写入器完成不同的转译功能,因为数据库类型和版本的不同,一个引用对象会对应多个写入器,因此在执行查询时就需要根据数据库的类型,选择对应的写入器,这个工作由执行配置器来完成。

执行配置器创建的查询转译器采用策略模式进行设计,查询转译器中注册了支持不同数据库SQL语法的多个写入器,值得注意的是,注册时先注册标准的ANSI SQL的写入器,然后再注册与数据库类型对应的处理差异化语法的写入器,最后注册处理特殊业务场景的写入器,根据写入器中定义的相同的可处理的元素类型,后注册的会覆盖之前注册的写入器。

优选地,通用数据访问框架中提供了ANSI SQL全套语法元素对应的写入器,以及处理常用数据库差异化语法的写入器,即能保证SQL转换的质量,又能方便开发人员使用,提高开发效率。

具体来说,写入器具有相同的父写入器接口HWriter;每一个写入器用于转译对应的数据库SQL中的一个语法元素;每一个引用对象至少对应一个写入器。写入器执行的操作包括:获取写入器处理类型、准备转译和写入转译;

①获取写入器处理类型,用于定义写入器可处理的元素类型,且与一个引用对象中定义的SQL语法元素类型对应;

需要说明的时,写入器中定义的元素类型,与一个引用对象中获取元素类型方法返回的元素类型对应,一个引用对象的元素类型可以对应多个写入器,即可以根据不同的应用场景,使用不同的写入器进行转译。

②准备转译,用于根据目标数据库类型,使用等价语法转换查询对象中的引用对象,调整引用对象之间的节点挂接关系,得到处理后的查询对象;

示例性地,MySQL5.7和MySQL8,版本不同,支持的函数也不同。如果要统计每一个产品的销量排名,对于MySQL8来说,采用RANK函数可以用于实现这个需求,但是如果使用MySQL版本是5.7,则没有提供RANK函数,只能采用定义变量和主子查询来获取排名。这种情况,可以自定义一个函数MY_RANK,该函数根据输入的排序方向和至少一个用于排序的列,得到排名,在构造语法树时,通过函数调用引用对象来调用函数MY_RANK,示例代码如下:

HQueryFacade.createQueryBuilder().startSelect()

.col(“P”,“PRODUCT_NAME”,“产品名称”)

.sel(HQueryFacade.getFunRef(“MY_RANK”,HQueryFacade.getCol(0,“TOTALS”),0),“RK”)

.endSelect().startForm.tab(“PRODUCT”,“P”).endFrom().build();

与函数调用引用对象对应的是函数调用写入器HWriterFun,其继承HWriter接口,扩展了一个获取函数名称的方法,则基于HWriterFun接口,针对不同类型和版本的数据库实现不同的写入器,比如MySQL5MyRankFunWriter、MySQL8MyRankFunWriter,在MySQL8MyRankFunWriter写入器的准备转译方法中不需要特殊处理,在写入转译方法中,转换成Rank函数的SQL片段即可,但是在MySQL5MyRankFunWriter写入器的准备转译方法中,由于排名函数是需要通过主子查询、增加排名字段来完成,则在写入器中将一个函数引用对象转换成多个引用对象重新挂接在主查询对象中,得到调整后的查询对象语法树。

③写入转译,用于根据处理后的查询对象中引用对象对应的元素类型,在当前上下文的字符串建造者中生成对应的SQL片段,和/或在参数列表缓冲区中写入值。

值得注意的是,如果写入器用来支持JPA的查询,则生成的是符合HQL(HibernateQuery Language,Hibernate查询语言)片段。

需要说明的是,上下文中包括一个字符串建造者StringBuilder,和一个参数列表缓存区List,根据写入器的处理功能和数据库类型进行处理。

示例性地,不同类型的数据库中表名、表别名、列别名的引号标识不同,比如,MySQL数据库中的引号要用单引号,Oracle数据库中要用双引号,那么在MySQL对应的写入器的写入转译方法中,在字符串建造者中生成对应的SQL片段时需要加上单引号,在Oracle对应的写入器的写入转译方法中,在字符串建造者中生成对应的SQL片段时需要加上双引号。

在进行查询时,如果通用数据访问框架中的写入器无法直接满足业务需求,可以通过继承父写入器接口来扩展新的写入器。

示例性地,常量引用对象对应的写入器有HCstWriterImpl,这个写入器负责在上下文的字符串建造者StringBuilder中写入占位符“?”,并且同时在上下文的参数列表缓冲区List中加入这个占位符对应的参数值,转译后,生成的SQL中自动带有占位符,而参数列表缓冲区中的参数值自动与占位符一一匹配。如果不想生成带有占位符的SQL,想让参数值直接拼接在SQL中,可以扩展一个写入器MyCstWriterImpl,在其实现中,将参数值直接写入当前上下文的字符串建造者StringBuilder中,不往上下文中的参数列表缓冲区中添加元素,使用这个转译器转译出来的SQL即是不带占位符“?”的了。最后只需要在转译器中注册新的写入器,替换HCstRef对应的写入器HCstWriterImpl即可。

S14:将查询对象传入查询执行器中,执行查询方法后得到查询结果。

需要说明的是,采用统一的查询逻辑构造的查询对象语法树,生成的查询对象传入查询执行器中,会由对应的查询执行器在查询方法中进行不同的处理。其中,JDBC执行器和JPA执行器的处理过程类似,对写入器转译生成的SQL或HQL进行查询,而内存执行器主要是对数据集合进行合并筛选,因此,分为两类进行说明。

①对于存储在数据库中的数据,当调用JDBC执行器或JPA执行器执行查询时,以查询对象的根引用对象对应的写入器作为查询转译器调用的入口写入器,在准备转译和写入转译中,根据其中定义的遍历顺序,分别对查询对象语法树进行遍历,对处理后的查询对象合并各写入器上下文中的字符串建造者和参数列表缓冲区,得到完整的SQL语句或HQL语句,以及参数列表,执行后得到查询结果。

具体来说,查询转译器在转译准备期和转译写入期分别对查询对象语法树进行遍历时,遍历顺序在入口写入器中定义。转译准备期主要用于调整语法树结构,只需要遍历语法树上的引用对象集合,以及集合内的每个引用对象即可,遍历顺序没有严格的要求,但是转译写入期需要按照SQL语法规范将多个写入器转译生成的SQL片段合并成完整的SQL语句和参数列表,在入口写入器中通过定义语法树引用对象的转译顺序,从而控制引用对象对应的写入器的调用顺序,创建上下文生成SQL片段,通过上下文合并顺序控制SQL片段和参数列表的合并顺序。

优选地,对于MySQL数据库,先转译From对应的表引用对象集合,在上下文中生成SQL片段,再转译Select对应的选择列引用对象集合,在上下文中生成SQL片段,合并SQL片段时,先合并写入了Select的上下文,再合并写入了From的上下文。

需要说明的是,根据语法树的层级,查询转译器对上下文的管理涉及多级上下文的操作,包括如下操作:

创建上下文,用于根据当前上下文生成子上下文,子上下文包括:当前上下文中的变量,子上下文独立的变量、字符串建造者和参数列表缓冲区;

合并上下文,用于将子上下文中的字符串建造者和参数列表缓冲区合并到父上下文中,最终合并生成完整的SQL语句和参数列表;

回收上下文,用于将空闲上下文释放至内存中的上下文资源池中。

示例性地,第一级的上下文派生出二级上下文,二级上下文派生出三级上下文,上下文合并时,三级上下文中的SQL片段和参数列表会合并到上级的上下文上。如果在合并三级上下文之前二级上下文中也存在写入SQL片段和参数,那么合并进来的三级上下文的内容一定会在二级上下文已写入的片段和参数之后。

在创建上下文时,上下文中的变量分为三种级别:全局变量、查询对象级变量和局部变量;其中,

全局变量,指在转译过程中所有上下文中共享的变量;

查询对象级变量,指在转译过程中查询对象的主子查询的上下文中分别独立但可共享的变量;

示例性地,在转译准备期,子查询的上下文中,将调整后的语法树写入查询对象级变量Map中,主查询可以从查询对象级变量Map中获取子查询调整后的语法树结构。

局部变量,指逐级向子上下文传递的变量,无法通过合并上下文方法写回父上下文中。

②对于存储在内存中的数据,当调用内存执行器执行查询时,获取查询对象的各引用对象集合,先获取表引用对象集合,对表引用对象排序、合并,得到宽表集合,再根据布尔条件引用对象集合对宽表集合进行筛选,接着根据排序引用对象集合对宽表集合进行排序,最后根据分组引用对象集合对宽表集合进行分组,得到查询结果。

需要说明的是,内存执行器可以使用内存数据源中的内存表对象,对其数据进行指定的逻辑处理。根据内存数据源和传入的查询对象,获取待查询数据,再筛选待查询数据、排序和分组待查询数据,得到最终的查询结果。

具体来说,在获取待查询数据时,需要考虑各种表关联场景,包括执行以下方法:

表排序,用于遍历查询对象中的表引用对象集合,根据表关联引用对象中左表为主表排序在前,右表为子表排序在后的规则,对表引用对象集合排序,得到排序后的表引用对象集合;

需要说明的是,在构造查询对象语法树时,是不分先后顺序的,而在表合并时是需要先加载了主表,才能基于主表合并子表,因此,通过排序方法,避免开发人员将子表构造在主表前面而导致后续表合并时找不到主表数据,而发生错误。

表合并,用于遍历排序后表引用对象集合,识别是否包含表关联引用对象,如果不包含表关联引用对象,获取内存表对象或者常量数据集,以笛卡尔积的形式,合并数据对象后放入宽表集合中;如果包含表关联引用对象,从表关联引用对象的子引用对象中获取关联关系、关联方式、关联的左表表名和右表数据,根据左表表名,从宽表集合中获取数据对象,按照关联方式和关联关系,将匹配的右表数据对象合并至宽表集合中。

需要说明的是,由于先进行了表排序,在进行合并时,会首先将主表即左表数据放入宽表集合中,在进行表关联引用对象处理时,可以根据左表表名从宽表集合中得到对应的数据对象。宽表集合是二维数组格式,二维数组的行数表示待查询数据的记录条数,每一行对应的每一列是一个对象,这种方式不用直接存储具体的数据信息,大大减少了存储空间。

优选地,采用list格式存储合并后的宽表集合;

根据关联方式left join、right join、inner join和full join,按照SQL中数据关联方式的查询规则来合并左右表数据。示例性地,以leftjoin为例,根据关联关系,取出右表中与左表匹配的数据对象,放入与左表数据对象同一行的对象数组中。

合并得到待查询数据后,筛选待查询数据是获取查询对象中where条件对应的布尔条件引用对象集合,将布尔条件引用对象集合中的布尔条件通过“and”连接成一个布尔条件,封装成一个布尔条件引用对象,再将封装的布尔条件引用对象转换成Java表达式,遍历宽表集合,将不符合Java表达式的数据对象直接从宽表集合中移除。

需要说明的是,由于表合并中已经将数据对象合并成了宽表集合,将布尔条件引用对象集合处理成一个布尔条件引用对象,只需要对宽表集合遍历一次,而且每取出宽表集合中的一行对象,就可以判断所有的筛选条件,处理效率高,性能消耗少。

示例性地,在查询对象语法树中存在如下条件:

条件1:startWhere().gtr(getCol(0,“ID”),getVal(1)).endWhere();//ID>1

条件2:startWhere().startOr().equ(getCol(0,“TYPE”),getVal(1))

.neq(getCol(0,“STATUS”),getVal(1))

.endOr().endWhere();//TYPE=1OR STATUS<>1

对条件1和条件2分别构造布尔条件引用对象,并放入布尔条件引用对象集合中,示例代码如下:

HBoolRef bool1=

new HBoolExpImpl(getCol(0,“ID”),GTR,getVal(1),null);

HBoolRefbool2=

new HBoolExpImpl(null,OR,Arrays.asList(

new HBoolExpImpl(getCol(0,“TYPE”),EQU,getVal(1),null),

new HBoolExpImpl(getCol(0,“STATUS”),NEQ,getVal(1),null),

),null);

List boolList=Arrays.asList(bool1,bool2);

将布尔条件引用对象集合中的布尔条件通过“and”连接成一个布尔条件,封装成一个布尔条件引用对象,示例代码如下:

HBoolRef lastbool=new HBoolExpImpl(null,AND,boolList,null);

相当于将2个条件组合成:ID>1AND(TYPE=1OR STATUS<>1)。

再将封装的布尔条件引用对象lastbool转换成Java表达式,遍历宽表集合,将不符合Java表达式的数据对象直接从宽表集合中移除。

优选地,采用Aviator表达式求值引擎将布尔条件引用对象转换成Java表达式,再编译成Expression的中间对象,遍历时,调用Expression的执行方法。

对筛选后的待查询数据进行排序和分组待查询数据,得到最终的查询结果,包括执行以下方法:

获取查询对象中的排序引用对象集合,基于责任链设计模式,通过JDK的排序函数对宽表集合进行排序;

获取查询对象中的分组引用对象集合,根据每个分组引用对象中定义的分组字段,依次从宽表集合中取出分组字段对应的数据,计算哈希值,并以哈希值为关键字,将哈希值相同的数据放入同一个集合,遍历结束后得到最终的查询结果。

优选地,在构造查询对象语法树时,如果设置了分页条件或者查询结果的最大条数,可以对上述分组后得到的查询结果再进一步过滤得到最终的查询结果。

对于不同执行器执行后得到的查询结果封装为实现了数据集接口的数据集对象,或者,封装为常量查询引用对象,用于内存数据查询中。

优选地,组合使用通用数据访问框架中的数据源和执行器。示例性地,对JDBC执行器或JPA执行器得到的多个查询结果,封装为内存表对象放入内存数据源中,或者封装为常量查询引用对象,然后构造查询对象语法树,生成的查询对象并传入内存执行器,实现对多个查询结果的整合、筛选、分组和排序。

与现有技术相比,本实施例提供了一种基于通用数据访问框架的数据查询方法,解决开发中针对不同存储形式的数据可以按照统一的SQL编写思维,使用链式方法调用来构造查询对象语法树,无需考虑SQL拼接顺序,并支持多层次子查询嵌套,支持复杂SQL的构造,降低了构造SQL的难度;只需构造一个查询对象,提供一套查询逻辑就可支持不同类型、不同版本的数据库、内存数据和常量数据集,提高了代码的复用度,数据库的兼容性,降低了维护成本;通过查询转译器集中处理不同数据库特殊转换,避免使用者编写不规范的原生SQL,避免SQL语法错误,提高了代码质量,降低了出现性能问题的风险;通过扩展自定义的函数、引用对象、写入器、执行器和数据集,满足个性化的使用。

本领域技术人员可以理解,实现上述实施例方法的全部或部分流程,可以通过计算机程序来控制相关的硬件来完成,所述的程序可存储于计算机可读存储介质中。其中,所述计算机可读存储介质为磁盘、光盘、只读存储记忆体或随机存储记忆体等。

以上所述,仅为本发明较佳的具体实施方式,但本发明的保护范围并不局限于此,任何熟悉本技术领域的技术人员在本发明揭露的技术范围内,可轻易想到的变化或替换,都应涵盖在本发明的保护范围之内。

技术分类

06120115927347