从ORM到OQM之3:查询对象和动态查询映射
1. 引言
在对使用if语句构建动态查询的代码进行重构后,我们实现了一种可以通过查询对象的实例直接构建动态查询的方法。这种方法包括以下步骤:
-
遍历查询对象的所有字段;
-
通过反射获取字段的赋值,根据已赋值字段的注解得到对应的查询条件,并将字段的赋值添加到参数列表。
-
使用
AND
将多个查询条件组合成查询子句。
借助这种方法,每新增一个查询条件,只需在查询对象中添加一个字段和声明了SQL条件的注解即可。 并且由于其通用性,构建动态查询的实现方法可以放在单独的仓库中进行维护和发布。
然而,随着查询条件的类型和数量的增加,通过注解声明查询条件的弊端也逐渐显现,主要存在以下问题:
-
查询条件中的列名和字段名称存在重复;
-
IN操作符的变长占位符需要特殊处理;
-
需要使用OR运算符连接多个查询条件;
-
需要支持子查询。
通过分析,我们发现操作符为等号的查询条件可以直接从字段名映射而得,从而避免在注解中重复编写SQL条件。这也让我们发现了消除SQL注解这个优化方向。
2. 查询条件归类
基于查询条件的特性,我们将其分为以下四类,每类查询条件由一种类型的字段映射得到:
-
包含列名、运算符和参数的基础查询条件
-
使用AND/OR连接的查询条件
-
含有子查询的查询条件
-
涉及多对多关系的查询条件
2.1. 谓词后缀字段
基础查询条件由列名、比较运算符和参数三部分构成,可以通过谓词后缀字段自动映射。
谓词后缀字段的命名格式通常为列名加上一个谓词后缀,这些后缀可以直接表示常见的比较运算符,例如,Eq代表等于=
、Gt代表大于>
等等。这种设计在DSL(领域特定语言)中非常常见。
使用谓词后缀对字段进行命名还有另一个好处。当需要为某列定义比较符不同的多个查询条件时,不同的谓词后缀可以避免字段间的命名冲突。
以UserQuery
类为例,原先通过@QueryField
注解声明查询条件的代码可以被简化成以下形式:
public class UserQuery { private String name; // name = ? private String nameLike; // name LIKE ? private Integer ageGt; // age > ? }
在根据谓词后缀构造查询条件时,我们还会根据字段的赋值情况动态生成占位符和处理参数。例如:
-
对于IN查询条件,可以根据参数个数自动生成对应个数的占位符;
-
对于LIKE查询条件,则根据谓词后缀为字段值自动添加
%
。详见附录A谓词后缀表的对应部分。
这种设计还让查询对象的构建更加直观。当为查询对象的字段赋值时,开发者可以直接根据字段名及其后缀的含义明确对应的SQL查询条件。
附录A中的谓词后缀表列出了常见的谓词后缀和SQL查询条件的映射关系,为使用这些字段定义提供了参考。
2.2. 逻辑后缀字段
由逻辑运算符AND或OR连接的多个查询条件,由逻辑后缀字段映射得到。 逻辑后缀字段的类型为集合或者查询对象,用于映射多个查询条件。 逻辑后缀字段的名称中包含逻辑后缀AND/OR,用于指定连接多个查询条件的逻辑运算符。
逻辑后缀字段的类型可以是查询对象或集合,其中每个字段或元素映射到一个查询条件,所有映射的条件由逻辑后缀确定的逻辑运算符组合而成。
这张表格展示了以Or为后缀的三种字段类型的逻辑后缀字段的映射示例:
字段 | 取值 (JSON格式) | 查询条件 |
---|---|---|
List<String> nameOr | ["test1","test2"] | name = ? OR name = ? |
UserQuery userOr | {"name": "John", "age": 30} | name = ? OR age = ? |
List<UserQuery> usersOr | [{"name": "test", "ageGe": 30, "ageLt": 40}, {"nameLike": "admin", "age": 40}] | (name = ? AND age >= ? AND age < ?) OR (name LIKE ? AND age = ?) |
例如,查询条件id = ? OR name = ? AND age > ?
,我们可以在UserQuery
类中添加字段userAnd
和userOr
,用于映射逻辑运算符连接的查询条件。
public class UserQuery { Integer id; String name; String nameLike; Integer ageGt; UserQuery userAnd; UserQuery userOr; }
再通过以下代码构建出UserQuery
的实例,由字段userOr
生成对应的查询条件:
UserQuery userAnd = UserQuery.builder().name("John").ageGt(30).build(); UserQuery userOr = UserQuery.builder().id(5).userAnd(userAnd).build(); UserQuery userQuery = UserQuery.builder().userOr(userOr).build(); List<Object> argList = new ArrayList<>(); String where = buildWhere(query, argList); // SQL: WHERE id = ? OR name = ? AND age > ?
在这个示例当中,同时存在逻辑运算符AND和OR,其中,AND的优先级高于OR。 由于逻辑运算符的运算优先级不同,我们认为SQL中的组合条件不是平面的,而是有层级的。 通过AND连接的查询条件位于一个层级, 通过OR连接的查询条件位于另一个层级。 不同层级的查询条件由相同的逻辑运算符组合而成。 基于以上发现,我们可以利用对象的层级关系来表达查询子句的层级关系。
下图展示了UserQuery
的实例中的赋值字段与查询条件的层级对应关系:
2.3. 子查询字段
对于一般的子查询条件,例如age > (SELECT avg(age) FROM t_user [WHERE])
,我们可以将其分为三个部分分别进行映射:
-
条件部分
age >
,我们可以复用谓词后缀字段的格式进行映射。但是为了避免和原有的谓词后缀字段ageGt产生命名冲突,我们需要在谓词后缀后再加一些字符进行区分,例如ageGtAvg。在映射时,忽略谓词后缀后的额外字符; -
子查询的主句部分
SELECT avg(age) FROM t_user
:将列名和表名作为不变的静态变量,通过注解声明,例如@Subquery(select = "avg(age)", from = "t_user")
; -
子查询的WHERE子句部分:通过复用查询对象映射方法进行构建。子查询字段的类型需要为查询对象。
通过这种方法,我们可以在UserQuery
中添加如下ageGtAvg
字段,用于构建上述查询条件:
public class UserQuery { // age > (SELECT avg(age) FROM t_user [WHERE]) @Subquery(select = "avg(age)", from = "t_user") UserQuery ageGtAvg; }
这样,我们仅使用一个字段便可以构建包含子查询的查询条件,且能为子查询构建动态查询语句。
2.4. 实体关系查询字段
为符合第三范式,对于具有多对多关系的两个实体,我们通常引入一个中间表,将多对多关系拆分为两个一对多关系。而这类中间表的表名和字段都具有特定的格式:表名包含两个实体的名称;两个外键也以entity_id的格式命名。这使得我们可以通过两个实体的名称就能推导出查询条件。
以RBAC模型为例,具有用户、角色和权限三个实体。其中,用户和角色具有多对多关系。其中间表的表名就可以由user和role推出,例如a_user_and_role,中间表的两个外键名称可以是user_id和role_id。那么,通过角色查询用户的查询条件为:
SELECT * FORM t_user WHERE id IN (SELECT user_id FROM a_user_and_role WHERE role_id IN ( SELECT id FROM t_role [WHERE]))
由于我们可以由实体名称user和role推出各自对应的表名,以及对应的中间表的表名和两个外键名称,进而推出上述的查询条件,我们通过表达式<user,role>
来表示上述查询条件。而对于role表后的WHERE子句,我们可以通过对实体role对应的查询对象复用查询对象映射方法得到。
由于角色和权限也具有多对多关系,这使得用户和权限也符合多对多关系的特征,我们使用表达式<user,role,perm>
来表示用户和权限通过角色形成的这种多对多关系,对应的推出根据权限查询所分配给的用户的查询语句为:
SELECT * FORM t_user WHERE id IN (SELECT user_id FROM a_user_and_role WHERE role_id IN ( SELECT role_id FROM a_role_and_perm WHERE perm_id IN ( SELECT id FROM t_perm [WHERE])))
由于表达式<user,role,perm>
在表示user和perm的关系时,形成了一条从user到perm的路径,并且路径的节点为具有多对多关系的实体的名称,所以我称之为抽象实体路径。
抽象实体路径可以包含任意多个节点来表示两个实体间的关系。例如,当系统中的权限和菜单也具有多对多关系时,可以使用表达式<user,role,perm,menu>
表示用户和菜单中间的多对多关系。以此类推。
根据以上分析,我们将通过角色和权限查询用户的字段分别定义如下:
public class UserQuery { //... @DomainPath({"user", "role"}) RoleQuery role; @DomainPath({"user", "role", "perm"}) PermQuery perm; }
根据角色名称查询所授予的用户,使用角色名称构建RoleQuery对象,再将其赋给UserQuery实例的role字段即可:
RoleQuery roleQuery = RoleQuery.builder().roleName("vip").build(); UserQuery userQuery = UserQuery.builder().role(roleQuery).build(); String where = buildWhere(userQuery, argList);
对应的查询子句如下:
WHERE id IN (SELECT user_id FROM a_user_and_role WHERE role_id IN ( SELECT id FROM t_role WHERE role_name = ?))
不仅如此,当抽象实体路径仅包含一个节点时,还能用于表示一对多/多对一关系,仅仅需要再指定一个外键的名称。
例如在菜单实体中,通过外键parent_id
连接父菜单的id,其查询字段看定义如下:
public class MenuQuery extends PageQuery { // many-to-one: query submenus by parent's conditions // parent_id IN (SELECT id FROM t_menu [WHERE]) @DomainPath(value = "menu", localField = "parentId") private MenuQuery parent; // one-to-many: query parent menus by children's conditions // id IN (SELECT parent_id FROM t_menu [WHERE]) @DomainPath(value = "menu", foreignField = "parentId") private MenuQuery children; }
我们可以把父子菜单的关系表示为<menu:menu|parentId,id>和<menu:menu|id,parentId>。
实体关系查询字段为我们提供了一种开箱即用的针对实体关系的查询模式。
3. 应用范围
由于查询对象的定义仅依赖于面向对象语言的基本特性,这使得这种方法能够适用于不同的面向对象编程语言。不同类型的数据库在查询条件的语法上具有一定的共性,因此,基于查询对象的映射方案不仅能够生成SQL语句,还能够为MongoDB等NoSQL数据库构建查询语句。
下图展示了如何通过统一的查询对象模型,构建适用于不同数据库的查询条件。该方法的通用性使得我们能够在多个数据库系统之间共享相同的查询构建逻辑,从而提高了代码的复用性和系统的灵活性。
查询对象对比图
4. 总结
本文详细介绍了如何基于查询对象实例自动生成动态查询条件,提出一种以字段特征映射查询条件的改进方法。该方法将查询条件抽象为四种字段类型以实现查询条件的自动构造。该方法减少了手动SQL编写的需求,大幅提高了代码的通用性和扩展性,为复杂查询提供了一种清晰高效的实现方案。
附录A:谓词后缀表
后缀名称 | 字段名称 | 字段赋值 | SQL查询条件 | MongoDB查询条件 |
---|---|---|---|---|
- | id | 5 | id = 5 | {"id":5} |
Eq | idEq | 5 | id = 5 | {"idEq":5} |
Ne | idNe | 5 | id != 5 | {"idNe":{"$ne":5}} |
Gt | idGt | 5 | id > 5 | {"idGt":{"$gt":5}} |
Ge | idGe | 5 | id >= 5 | {"idGe":{"$gte":5}} |
Lt | idLt | 5 | id < 5 | {"idLt":{"$lt":5}} |
Le | idLe | 5 | id <= 5 | {"idLe":{"$lte":5}} |
In | idIn | [1,2,3] | id IN (1,2,3) | {"id":{"$in":[1, 2, 3]}} |
NotIn | idNotIn | [1,2,3] | id NOT IN (1,2,3) | {"id":{"$nin":[1, 2, 3]}} |
Null | memoNull | false | memo IS NOT NULL | {"memo":{"$not":{"$type", 10}}} |
Null | memoNull | true | memo IS NULL | {"memo":{"$type", 10}} |
NotLike | nameNotLike | "arg" | name NOT LIKE '%arg%' | {"name":{"$not":{"$regex":"arg"}}} |
Like | nameLike | "arg" | name LIKE '%arg%' | {"name":{"$regex":"arg"}} |
NotStart | nameNotStart | "arg" | name NOT LIKE 'arg%' | {"name":{"$not":{"$regex":"^arg"}}} |
Start | nameStart | "arg" | name LIKE 'arg%' | {"name":{"$regex":"^arg"}} |
NotEnd | nameNotEnd | "arg" | name NOT LIKE '%arg' | {"name":{"$not":{""}}} |
End | nameEnd | "arg" | name LIKE '%arg' | {"name":{""}} |
NotContain | nameNotContain | "arg" | name NOT LIKE '%arg%’ | {"name":{"$not":{"$regex":"arg"}}} |
Contain | nameContain | "arg" | name LIKE '%arg%’ | {"name":{"$regex":"arg"}} |
Rx | nameRx | "arg\d" | name REGEXP 'arg\d’ | {"name":{"$regex":"arg\d"}} |
附录B:GitHub
Java version: http://github.com/doytowin/doyto-query
Golang version: http://github.com/doytowin/goooqo

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
深入Java泛型及其设计原则
在日常开发中,必不可少的会使用到泛型,这个过程中经常会出现类似“为什么这样会编译报错?”,“为什么这个列表无法添加元素?”的问题,也会出现感叹Java的泛型限制太多了很难用的情况。为了更好的使用泛型,就需要更深的了解它,因此本文主要介绍泛型诞生的前世今生,特性,以及著名PECS原则的由来。 泛型的诞生 ▐背景 在没有泛型之前,必须使用Object编写适用于多种类型的代码,想想就令人头疼,并且非常的不安全。同时由于数组的存在,设计者为了让其可以比较通用的进行处理,也让数组允许协变,这又为程序添加了一些天然的不安全因素。为了解决这些情况,Java的设计者终于在Java5中引入泛型,然而,正是因为引入泛型的时机较晚,为了兼容先前的代码,设计者也不得不做出一些限制,来让使用者(也就是我们)以难受换来一些安全。 ▐优点 简单来说,泛型的引入有以下好处: 程序更加易读 安全性有所保证 以ArrayList举例,在增加泛型类之前,其通用性是用继承来实现的,ArrayList类只维护一个Object引用的数组,当我们使用这个工具类时,想要获取指定类型的对象必须经过强转: import java.uti...
- 下一篇
2024年“中国高校计算机大赛-智能交互创新赛”圆满收官
11月16日,第三届(2024年)中国高校计算机大赛——智能交互创新赛在浙江省杭州市举行了全国总决赛及颁奖典礼。本届竞赛以“交互无界,创意无限”为主题,聚焦人机交互技术,深度挖掘AI智能体的应用潜力,推动智能产业的升级和发展。 自4月17日启动以来,本届竞赛吸引了来自全国33个省/直辖市、近445所高校的3256支团队,共计4256名同学报名参赛,历经7个月的初赛、复赛、决赛的激烈角逐,在今日进行了最终的对决。同时,本届的参赛作品也更具创新性与实用性,涵盖了教育、生活、情感、创作、医疗、旅游、美食等多个领域,引导学生在多样化的学习、工作和生活场景中探索智能体创新交互的方式和潜能。 创新方案云集,AI智能体交互获新突破 11月16日,40支总决赛队伍围绕“大模型时代的 AI 智能体创新交互”竞赛主题,通过两轮线下路演答辩,展示了其对新一代人机交互的创新思考和解决方案。最终,经过竞赛评审专家组公平、公正、公开的评判,大赛共产生一等奖队伍3支,二等奖队伍6支,三等奖队伍10支,优胜奖队伍18支,优秀指导老师15位。 其中,来自哈尔滨工业大学的参赛作品《雅韵智诵——AI智能背诵助手》荣获竞赛特...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7设置SWAP分区,小内存服务器的救世主
- Mario游戏-低调大师作品
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- 2048小游戏-低调大师作品
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7,CentOS8安装Elasticsearch6.8.6