关于jOOQ的10件你不知道的事

912 阅读13分钟

jOOQ已经存在了一段时间--从2009年左右作为一个公开可用的库,到2013年作为一个商业授权产品。

12年中发生了很多事情。这里有10件你可能不知道的关于jOOQ的事情。

1.eq、ne、gt、ge、lt、le的灵感来自XSLT

有什么比命名一个局部变量更难的?

我在想那个局部变量的名字pic.twitter.com/cRDAmFfY9E

- Lukas Eder (@lukaseder)February 25, 2021

命名公共API!最初的jOOQ有这样的方法来构造谓词:

  • [Field.equal(Field)](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Field.html#equal(org.jooq.Field))
  • [Field.notEqual(Field)](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Field.html#notEqual(org.jooq.Field))
  • 等等

但诸如greaterOrEqual() 等词在本应是一个非常可读的、SQL风格的DSL中间有点 "沉重",所以需要添加一个短版本。但是该如何命名这个简短的版本呢?有不同的意见。

因为我对XSLT的热爱几乎和对SQL的热爱一样,所以很自然地选择eq,ne,lt,le,gt,ge 作为这些操作符,就像在XSLT中一样。嗯,准确地说,是XPath。这些也可以作为HTML实体使用。

有趣的是,从jOOQ开始,我们也支持在SQL解析器中解析这些缩写,以支持替代的Teradata语法,见github.com/jOOQ/jOOQ/i…(是的,全能的Teradata!):

-- Valid in Teradata, I mean, why not?
SELECT *
FROM t
WHERE id EQ 1

其他类似的库使用eq,ne,lt,loe,gt,goe 。但我知道这些非常相似的运算符的名字长度不一致,晚上就睡不着觉。

2.API命名的遗憾

有些名字,我希望我没有选择。最突出的三个是:

  • [Field](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Field.html)(它实际上更像是一个列或列表达式。字段听起来很像MS Excel)
  • [Condition](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Condition.html)(SQL标准称其为predicate)
  • [Record](https://www.jooq.org/javadoc/dev/org.jooq/org/jooq/Record.html)(这个名字很好听,但现在和java.lang.Record 冲突,这很麻烦。如果我当时叫它Row ,就好了 )

唉,这些术语不可能在不破坏每一个jOOQ应用程序的情况下被改变,没有什么好理由。这就是一个API开发者的生活。

3.3.jOOQ的API中的重载实际上是没有标记的联合类型

哦,如果Java更像TypeScript就好了。他们有这些漂亮的一流的无标记联合类型,而我们这些可怜的Java人只能从异常捕捉块中了解到这些类型,在那里它们并不是作为一流的语言特性而存在,而只是作为语法上的糖:

type F<T> = String | Name | Field<T> | Select<? extends Record1<T>>

如果 "只是 "我们在Java中拥有这些......(以及上述类型别名)。那么维护像jOOQ这样的庞大API的所有麻烦就会消失,也就是永远存在的重载方法集。

看看DSL::substring 的重载就知道了。看看:

[

而且这还没有涵盖所有可能的变化,到目前为止。一个务实的决定是,第一个参数不太可能是一个字符串绑定的变量。我们几乎不支持Name ,除非这个参数真的是关于一个列的引用,而不是一个任意的列表达式,而标量子查询的情况(Select<? extends Record1<T>>)好吧,那只是方便。

如果用户仍然需要绑定变量,他们可以用以下方式来包装它 [DSL.val("value")](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/impl/DSL.html#val(java.lang.String)).不过,如果Java真的有无标记的联合类型,那么API就会看起来更像这样:

static Field<String> substring(
    F<String> string,
    F<? extends Number> startingPosition,
    F<? extends Number> length = null
) { ... }

其中F 是上述联合类型。这将为像jOOQ这样的DSL增加很多便利,因为现在,每个 参数类型的排列组合都被支持。唉,我们在这里,通过大量的重载方法手工滚动模拟联合类型,希望IDE的自动完成不会失败或变得太慢(hello IntelliJ / Kotlin)😅。

4.一些SQL的东西很难被抽象出来

即使过了12年,在试图将一些SQL功能组合转化为目前支持的30种RDBMS时,仍然有一些难以解决的错误。

我的意思是,在万能的PostgreSQL上试试这个,它甚至没有被我创造非日常SQL的蹩脚尝试所打动:

with t (a) as (
  with u (b) as (
    values (1)
  )
  select c
  from (
    with v (c) as (select b from u)
    select c from v
    union (
      select c from v
      union all
      select c from v
      order by c
    )
    order by c
  ) v
)
select a from t

这只是一种混淆视听的写法:

values (1)

这里有几件事:

  • 我们在CTE声明中嵌套CTE
  • 我们在派生表中嵌套CTE
  • 我们在派生表中嵌套联合体
  • 我们在联合子查询中嵌套联合体
  • 我们对派生表进行排序
  • 我们对联合子查询进行排序

我的意思是,为什么不呢?但是其他方言呢?如果你有胆量的话,可以在www.jooq.org/translate/,尝试将其转换为其他方言。这些子弹中的每一个(或者一次有几个),在某些 方言中都不工作。而且还不是所有的翻译都能工作,有很多原因。

这些都不是最重要的错误。它们通常是边缘情况(例如,ORDER BY 子句是没有意义的),但即使如此,你也希望尽可能多的SQL在所有的方言上工作,所以我们一直在忙,这是肯定的。

5.发音

现在是官方的了(一直以来都是这样):

它的发音是dʒuːk(如juke)。

jOOQ是一个递归的缩写,代表jOOQ面向对象的查询。面向对象 "代表API的设计,而不是你应该如何使用它。你应该以函数式编程风格来使用它,咄。如果你愿意的话,jOOQ表达式树是遵循复合模式的,而SQL生成是使用访问者模式风格的方法实现的,一切都被封装起来了,等等。就像你不应该被诱惑说ess queue ell,你也不应该被诱惑说jay o o queue。这只是dʒuːk续集。Hah!

6.RDBMS的错误

jOOQ已经帮助发现了大量的RDBMS错误,甚至超过了令人敬畏的github.com/sqlancer/sq…当jOOQ集成了一个新的方言(例如EXASOL,最近),我们发现了大量的错误。在这里可以看到一个列表:https://github.com/jOOQ/jOOQ/issues/1985,或者对于最近支持的Apache Ignite:https://github.com/jOOQ/jOOQ/issues/10551。

这是因为我们的集成测试非常庞大,涵盖了各种奇怪的语法组合,几乎没有人担心过,比如前面的第4项。我总是把我发现的每个bug记录下来,如果它是公开的,就记录在RDBMS问题跟踪器上,或者记录在Stack Overflow上。

所以,如果你是一个RDBMS供应商,并希望我们测试你的SQL实现,请告诉我们!我们是雇佣的。我们是可以雇佣的。

7.可变性是一个错误

jOOQ中最大的API设计错误之一是DSL的可变性,现在几乎无法删除。不兼容地改变行为甚至比API更难。当API发生不兼容的变化时,会出现编译错误。它们是一种痛苦,但至少不会有任何意外。

对于一个库来说,改变行为是一个很大的禁区。这就是我所说的:

SelectWhereStep<?> s =
ctx.select(T.A, T.B)
   .from(T);

// Dynamic SQL how you shouldn't do it:
if (something)
    s.where(T.C.eq(1));

if (somethingElse)
    s.where(T.D.eq(2));

Result<?> result = s.fetch();

是的,DSL的API是可变的,这就是为什么上面的工作。它不应该工作,你也不应该使用这个,但我们在这里。我们自己的小sun.misc.Unsafe 灾难。这已经太晚了。每个人都已经在使用它了。

不是所有的DSL元素都是可变的,例如,表达式就不是:

Condition c = noCondition();

// Has no effect
if (something)
    c.and(T.C.eq(1));

if (somethingElse)
    c.and(T.D.eq(2));

上面的方法是行不通的。你很快就会注意到,并据此进行修正:

Condition c = noCondition();

if (something)
    c = c.and(T.C.eq(1));

if (somethingElse)
    c = c.and(T.D.eq(2));

所以,还是有变异的,但只是你的 本地变量,而不是任何jOOQ对象。这就是整个DSL应该工作的方式。或者更好的是,你可以使用函数式编程风格来实现动态SQL

在任何情况下,要想在不破坏一切的情况下改变这种行为是非常困难的,因为你不可能轻易地检测到可变的API使用情况。在jOOQ 3.15中,我们已经开始用@CheckReturnValue 注解来注解DSL API,这已经被一些工具和IDE所接受,例如,见youtrack.jetbrains.com/issue/IDEA-…

幸运的是,当你以可变的方式使用DSL API时,这个注解也会引起警告,因为你应该消费那个where(T.C.eq(1)) 的返回值。也许毕竟有办法改变这一点,虽然机会不大。可能不值得造成损害。

是的,这就是每个 "轻量级库 "的命运,一旦它成熟了。几乎不可能再改变它的基本缺陷了:

每一个新的库:

第一年:这个轻量级的库比<沉重而复杂的竞争对手>好得多
第十年:增加这个小功能只需要重构,设计大修,以及这500个递减pic.twitter.com/M4IwKof8D8

- Lukas Eder (@lukaseder)2021年8月19

8.源码和行为的兼容性在jOOQ中非常重要

行为上的不兼容

行为上的不兼容在几乎所有的库/产品中都是绝对不允许的。

源码不兼容

源码不兼容有时是不可避免的,至少在主要版本中,如果有一个非常令人信服的理由。由于我们已经有近十年没有发布过大版本了,所以我们把小版本当作大版本。

在jOOQ中这种不兼容的例子是,我们删除了接受Condition|Field<Boolean>|Boolean 的便利重载。这三样东西在jOOQ中是一样的:

  • Condition 是一个SQL谓词(见第2项)
  • Field<Boolean> 是一个被包装成 的 ,这在对 类型有本地支持的方言中是非常好的。Field<Boolean> Condition BOOLEAN
  • Boolean 只是一个布尔绑定变量,被包装成DSL.val(boolean)

那么,有什么问题呢?问题在于Boolean 的重载。它是为这些类型的使用而设计的:

// Turn off the entire query or subquery, dynamically
boolean featureFlag = ...;
.where(featureFlag)

所以,很少有用,只是在边缘情况下。再说一遍,问题出在哪里?问题是,用户不小心写了这个所有的时间:

.where(USER.NAME.equals(userName))

你能发现这个错误吗?如果他们写的是USER.NAME.eq(userName) ,也就是说,如果他们使用XSLT风格的缩写,就不会发生这种情况。

是的,他们写的是equals() (如 [Object::equals](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/Object.html)),而不是equal() (如 [Field::equal](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/Field.html#equal(org.jooq.Field))).只是打字错误。还是被编译了。选择了错误的重载。而且看起来几乎正确。这些本来是正确的:

.where(USER.NAME.equal(userName))
.where(USER.NAME.eq(userName))

因此,我们废除了这个重载,然后删除了这个重载,这样,现在使用Object::equals ,就会出现编译错误。似乎是一个合理的破坏源代码的案例,因为那个API的使用几乎都是偶然的。

如何测试这些东西?

为了确保我们不会破坏行为或源代码的兼容性,我们有一大堆(我是说一大堆)的测试。行为兼容性是通过单元和集成测试来检查的,确保jOOQ API的复杂和奇怪的使用在我们支持的所有RDBMS上继续产生相同的结果。

源兼容性是通过大量手写的jOOQ API使用情况来检查的,这可能比你的一般使用情况更冗长。例如,我们在自己的测试中不怎么使用var ,尽管我们强烈建议 在你的客户端代码中使用它:

var result = ctx.select(T.A, multiset(..).as("fancy stuff")).fetch();

相反,即使是在使用newMULTISET 操作符的最花哨的语句中,这可能对jOOQ的结构类型滥用相当严重,我们总是把所有东西都分配给明确类型的变量,这些变量可能看起来像这样的威胁:

Result<Record4<
    String,                   // FILM.TITLE
    Result<Record2<
        String,               // ACTOR.FIRST_NAME
        String                // ACTOR.LAST_NAME
    >>,                       // "actors"
    Result<Record1<String>>,  // CATEGORY.NAME
    Result<Record4<
        String,               // CUSTOMER.FIRST_NAME
        String,               // CUSTOMER.LAST_NAME
        Result<Record2<
            LocalDateTime,    // PAYMENT.PAYMENT_DATE
            BigDecimal        // PAYMENT.AMOUNT
        >>, 
        BigDecimal            // "total"
    >>                        // "customers"
>> result = 
dsl.select(...)

即使该类型实际上没有被消耗或以任何方式使用,例如在对其调用result.formatXML() 。更多的测试=更好。任何源码的不兼容都会立即导致我们的测试停止编译,所以我们可以确信不会遇到任何意外。

尽管如此,总是有一些奇怪的事情。要做到完美是很难的。一个例子是这个问题,rawtype的兼容性被忽略了。我想知道是否有人真正确保他们的泛型可以安全地与原始类型兼容?在jOOQ的案例中,似乎非常困难...

9.在jOOQ中几乎不可能提供二进制兼容性。

但是二进制不兼容?在像jOOQ这样的DSL中,它们可能是完全不可能的。一个例子是,我们在jOOQ 3.12中引入了对TeradataQUALIFY 条款的支持这里有一篇博文,解释了我们的DSL是如何工作的。在支持QUALIFY 之前,jOOQ的WINDOW 子句,通过 [SelectWindowStep](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/SelectWindowStep.html)有几个window() 方法的重载,比如这样:

// "extends SelectOrderByStep", because is window() methods are
// obviously optional. It's a hardly used feature
interface SelectWindowStep<R extends Record> 
extends SelectOrderByStep<R> {
    SelectOrderByStep<R> window(WindowDefinition... w);
}

使用实例:

select(T.A, count().over(w))
.from(T)
.window(w)
.orderBy(T.A)
.fetch();

现在,QUALIFY 子句出现在WINDOW 之后(Teradata不支持WINDOW ,但如果他们支持,就必须把它前置到QUALIFY ,因为WINDOW 声明了命名的窗口定义,而QUALIFY 会消耗它们,尽管你永远不知道SQL的情况):

// "extends SelectQualifyStep" is a compatible change
interface SelectWindowStep<R extends Record> 
extends SelectQualifyStep<R> {

    // But these changes are not
    SelectQualifyStep<R> window(WindowDefinition... w);
}

虽然JVM允许通过返回类型进行重载(从Java 5开始,这一特性被用来实现泛型和共变重载),但Java语言不允许这样做。这个API的新版本不可能维持旧的字节码,至少在Java中不可能。Kotlin支持这样的事情,即能够在字节码中发射(合成?)不能从源代码中直接调用的方法,纯粹是为了向后兼容的原因。

所以,如果你从jOOQ 3.11升级到3.12,而你使用的是WINDOW 子句,运气不好。你必须重新编译你的客户端代码,否则你的朋友NoSuchMethodError ,只有在运行时才有话语权(因为在编译时,一切都还在编译中)。

我想,在大多数情况下,二进制兼容性已经不是什么大问题了。我们一直在运行CI/CD工作,并且一直在出于各种原因重新编译一切。你不太可能保持你自己的内部库的二进制兼容,让其他团队来使用。尽管如此,如果这不是一个问题就好了。最终,实用主义决定了我们不能在jOOQ中提供这种服务(比如JDK所做的),至少在补丁版本中要非常努力地不破坏二进制兼容,但次要版本则没有任何这样的保证。

10.徽标

目前的标志是在jOOQ进入商业化时设计的:

[jOOQ is now jOOQ™

有趣的是,这两个logo都是我自己设计的。我的妻子从来不喜欢旧的那个。在她看来,它就像一个血淋淋的武士,眼睛都是血色的😅。

新的那个经历了几次迭代。所有这些都很奇怪,色彩鲜艳。最后,一个从事设计工作的朋友告诉我一些非常简单的技巧:

  • 与彩色标识相比,黑白可以非常容易地应用于所有媒体(屏幕、印刷品等)。
  • 它甚至可以倒置成白色和黑色。
  • 在图标、标题、缩略图等方面,正方形比长方形更容易管理。

我在此基础上更进一步,将整个标志做成了四边形,包括字体,这样它甚至不需要SVG格式来缩放。它本质上是一个20×20的位图。没有比这更低的预算却更有效的了!😁