从jOOQ 3.16开始,我们在开放我们的内部查询对象模型(QOM)作为公共API方面投入了很多。这主要对那些使用jOOQ的解析器并希望访问解析后的表达式树的人有用,或者对SQL进行转换,例如在jOOQ中实现行级安全。
但偶尔,即使在普通的jOOQ动态SQL使用中,访问表达式树也是有用的:
请注意,从jOOQ 3.16开始,所有这些新的API都是实验性的,因此在未来会有不兼容的变化。使用它的风险由你自己承担。
查询对象模型(QOM)的API
第一个改进是为查询对象模型本身提供一个API。一个新的类型叫做 [org.jooq.impl.QOM](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/impl/QOM.html)的新类型包含了所有这些新的公共API,而实现仍然是相同的旧类型,在org.jooq.impl 包中,但具有包-私有的可见性。
当你创建一个SUBSTRING() 函数调用表达式时,你会得到一个Field<String> 表达式,它实现了 [QOM.Substring](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/impl/QOM.Substring.html).在这个类型上,你可以调用各种访问器方法,这些方法总是以"$" 符号开头,以访问函数参数:
// Create an expression using the DSL API:
Field<String> field = substring(BOOK.TITLE, 2, 4);
// Access the expression's internals using the model API
if (field instanceof QOM.Substring substring) {
Field<String> string = substring.$string();
Field<? extends Number> startingPosition =
substring.$startingPosition();
Field<? extends Number> length = substring.$length();
}
一些可能会被改变的东西
1.目前还不清楚DSL方法substring() 是返回QOM类型Substring ,还是DSL类型Field 。两者都有优点和缺点,尽管对于DSL用户来说,有一点倾向于让QOM类型不被看到。
2.2."$" 前缀用于明确区分DSL API(无前缀)和QOM API("$" 前缀),因为现在这两个API共享类型层次,用户应该清楚他们是在构建jOOQ对象用于DSL,还是在操作表达树的对象。
对于每个访问器,也有一个 "突变器",这个方法产生一个包含突变值的新QOM类型。所有的QOM类型都是不可改变的,所以原始的Substring实例不会受到像这样的修改的影响:
Substring substring1 = (Substring) substring(BOOK.TITLE, 2, 4);
Substring substring2 = substring1
.$startingPosition(val(3))
.$length(val(5));
assertEquals(substring2, substring(BOOK.TITLE, 3, 5));
上述所有的API、访问器和突变器都将适用于所有的jOOQ版本,包括jOOQ开源版。
表达式树的遍历
当你想遍历表达式树时,真正的乐趣就开始了,例如,寻找对象的存在,收集对象,等等。为此,我们在商业版jOOQ中引入了新的 [Traverser](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Traverser.html)为此,我们在商业版的jOOQ中引入了新的API。
Traverser的工作方式与JDKCollector 相当类似,它可以遍历一个Stream,并将元素收集到一些数据结构中。但是Traverser在树上操作,因此有一些额外的功能:
- 它可以在访问一个树状元素(以及它们的子树!)之前和之后接收事件。
- 它可以为每个树元素决定是否应该向子树递归遍历。这是非常有用的,例如,如果你不关心任何种类的子查询的遍历。
- 它可以决定是否提前终止遍历,例如,当找到第一个对象时。我不知道JDK
Collector是否提供这样的短路方法,尽管我认为这在那里也很有用。(用Spliterator也可以,但那要麻烦得多) - 它不具备并行能力。并行性已经是流的一个可选的特性,但是对于树,我们还没有发现支持并行性的好处,这使得遍历更加简单。
一个简单的遍历例子是计算一个表达式中的所有QueryPart对象,像这样:
// Contains 7 query parts
System.out.println(
T_BOOK.ID.eq(1).or(T_BOOK.ID.eq(2))
.$traverse(() -> 0, (c, p) -> c + 1)
);
这个简单的方便方法提供了一个辅助数据结构(这里是一个int),以及一个将每个查询部分累积到该数据结构的函数。其结果是数据结构(int)本身。
为什么它要计算7呢?因为它遍历了以下的树:
1: T_BOOK.ID.eq(1).or(T_BOOK.ID.eq(2))
2: T_BOOK.ID.eq(1)
3: T_BOOK.ID
4: 1
5: T_BOOK.ID.eq(2)
6: T_BOOK.ID
7: 2
或者从视觉上看:
OR
├── EQ
│ ├── T_BOOK.ID
│ └── 1
└── EQ
├── T_BOOK.ID
└── 2
如果你想简单地收集每个单独的QueryPart ,就这样做:
System.out.println(
T_BOOK.ID.eq(1).or(T_BOOK.ID.eq(2))
.$traverse(
() -> new ArrayList<QueryPart>(),
(list, p) -> {
list.add(p);
return list;
}
)
);
这样做的输出结果是(不是本地格式):
[ ("PUBLIC"."T_BOOK"."ID" = 1 or "PUBLIC"."T_BOOK"."ID" = 2), "PUBLIC"."T_BOOK"."ID" = 1, "PUBLIC"."T_BOOK"."ID", 1, "PUBLIC"."T_BOOK"."ID" = 2, "PUBLIC"."T_BOOK"."ID", 2]
这个例子表明,该树是以深度优先的方式进行遍历的。
但是你不需要自己写这样简单的Traversers。任何JDKCollector都可以作为Traverser ,所以上面的两个例子可以这样改写:
System.out.println(
T_BOOK.ID.eq(1).or(T_BOOK.ID.eq(2))
.$traverse(Traversers.collecting(Collectors.counting()))
);
System.out.println(
T_BOOK.ID.eq(1).or(T_BOOK.ID.eq(2))
.$traverse(Traversers.collecting(Collectors.toList()))
);
想收集一个查询的所有涉及的表?没问题!
System.out.println(
T_BOOK.ID.eq(1).and(T_AUTHOR.ID.eq(3))
.$traverse(Traversers.collecting(
Collectors.mapping(
p -> p instanceof TableField<?, ?> tf
? tf.getTable()
: p,
Collectors.filtering(
p -> p instanceof Table,
Collectors.toSet()
)
)
))
);
这可以理解为:
- 将所有的
TableField引用映射到它们的Table容器中 - 过滤掉所有
Table的引用 - 将它们收集到一个不同的
Set的表。
生成:
["PUBLIC"."T_BOOK", "PUBLIC"."T_AUTHOR"]
表达式树的转换
如果你想用一个表达式替换另一个表达式怎么办?有各种用例,我们最终会在商业的jOOQ版本中支持这些用例,但你也可以使用这个API推出自己的扩展。
这种转换的一个非常简单的例子是去除多余的布尔否定:
// Contains redundant operators
Condition c = not(not(BOOK.ID.eq(1)));
System.out.println(c.$replace(q ->
q instanceof Not n1 && n1.$arg1() instanceof Not n2
? n2.$arg1()
: q
));
尽管明确写了not(not(x)) ,但输出只是x ,或者具体地说:
"BOOK"."ID" = 1
这种转换的实际应用案例包括。
常见模式的优化和替换
有几个理由来规范和改进SQL字符串的常见模式:
- 优化,以防止支持的RDBMS无法处理它。请看我们的文章:10个不依赖于成本模型的SQL优化,例如消除连接。
- 风格上的改进,可能不会对性能产生任何明显的影响,比如通过
UPPER(x),取代多余的函数调用,UPPER(UPPER(x))。 - 类似SQL的规范化,以帮助使用jOOQ的DiagnosticsListener检测重复的SQL字符串,以防止执行计划缓存的争夺。
从jOOQ 3.17开始,我们将提供很多这样的转换,开箱即用。你可以出于不同的原因将它们打开:
- 普遍优化你的SQL输出
- 检测你的查询中的问题,无论是通过jOOQ API实现的,还是通过解析器拦截的--经验法则是,如果这个模式识别功能找到了要转换的东西,那么你自己的SQL查询就应该得到改进。可以这么说,是一个inter。
从jOOQ 3.17开始的开箱即用功能:https://github.com/jOOQ/jOOQ/issues/7284
行级安全或共享模式多租户
你今天已经可以使用jOOQ的VisitListenerSPI实现客户端行级安全,这是这些基于新查询对象模型的SQL转换功能的前身。但有了新的替换API,对于用户以及我们来说,支持开箱即用的行级安全功能将变得更加简单。简而言之,想象一下,每次你查询一个受限制的表,如ACCOUNT:
SELECT * FROM account
你想要的是确保用户只能访问他们自己的账户,也就是说,这应该被修补到查询中,对开发者来说是透明的:
SELECT * FROM account WHERE account_id IN (:userAccountList)
一个简单的算法是这样写的:
QueryPart q = select(ACCOUNT.ID).from(ACCOUNT);
System.out.println(
q.$replace(p -> {
if (p instanceof Select<?> s) {
// Check if the query contains the relevant table(s) in
// the FROM clause
if (s.$from().$traverse(Traversers.containing(ACCOUNT)) && (
// In the absence of a WHERE clause
s.$where() == null ||
// Or, if we haven't already added our IN list
!s.$where().$traverse(Traversers.containing(
x -> x instanceof InList<?> i
&& ACCOUNT.ID.equals(i.$arg1())
))
)) {
// Append a predicate to the query
// Imagine this reading some context info
return s.$where(DSL.and(s.$where(),
ACCOUNT.ID.in(1, 2, 3)));
}
}
return p;
})
);
上面的结果将是:
select "PUBLIC"."ACCOUNT"."ID"
from "PUBLIC"."ACCOUNT"
where "PUBLIC"."ACCOUNT"."ID" in (
1, 2, 3
)
请注意,输入的SQL查询并不包含任何这样的谓词。很明显,这还远远不够完整。它没有正确地处理外部连接(其中谓词可能必须进入ON子句),以及其他注意事项。请继续关注该领域的更多信息。
还没有发布目标的开箱即用功能:
github.com/jOOQ/jOOQ/i…
更多用例
在上述功能的基础上,我们还计划支持更多的使用情况,即开箱即用。这些包括:
- 软删除,将
DELETE语句转化为 "等价的 "UPDATE .. SET deleted = true语句,以及将SELECT语句转化为 "等价的 "SELECT .. WHERE NOT deleted,见github.com/jOOQ/jOOQ/i… - 审计列支持,只要任何DML查询触及 "审计 "字段,如
CREATED_AT,CREATED_BY,MODIFIED_AT,MODIFIED_BY,我们就会更新它们,见github.com/jOOQ/jOOQ/i…
用例敏捷性
请记住,像大多数其他jOOQ功能一样,这个功能也是完全不受用例影响的。你是否在使用jOOQ并不重要:
- 作为一个内部DSL来创建动态(或 "静态")SQL查询
- 作为一个解析器,在SQL方言之间进行翻译
- 作为一个分析器来丰富你基于传统ORM的应用程序
- 作为一个诊断工具,对你基于传统ORM的应用程序进行检查
不管是什么用途,你都可以使用这个API来分析和转换你的SQL查询。
限制(截止到jOOQ 3.16)
如前所述,到目前为止,这是一个实验性功能,还没有真正为生产做好准备。目前的设计和实现有相当多的已知限制。请考虑这个问题的开放性问题。
到目前为止,最重要的限制包括:
- 只支持SELECT,没有其他语句
- 遍历还没有进入
JOIN树或UNION/INTERSECT/EXCEPT子查询
还有更多的限制,但这些是最重要的限制。因此,请继续关注这一领域的更多令人兴奋的发展,在下一个jOOQ版本中很快就会出现。