jOOQ NOT NULL 列转换器:如果列不能为空,为什么需要处理 null?

Rob*_*gan 3 converters jooq jooq-codegen

jOOQ 代码生成中,可以将转换器分配给NOT NULL字段,如下所示:

<forcedType>
    <includeTypes>(?i)^varchar\(\d+\)$</includeTypes>
    <userType>String</userType>
    <nullability>NOT_NULL</nullability><!-- Converter applies only to NOT NULL columns! -->
    <converter>StringCaseConverter</converter>
</forcedType>
Run Code Online (Sandbox Code Playgroud)

然后转换器可以像这样实现:

public class StringCaseConverter extends org.jooq.impl.AbstractConverter<String, String> {
    public StringCaseConverter() {
        super(String.class, String.class);
    }

    @Override
    public String from(String databaseObject) {
        return databaseObject.toLowerCase(); // FIXME: this throws NPE if argument is ever null!
    }

    @Override
    public String to(String userObject) {
        return userObject.toUpperCase(); // FIXME: this throws NPE if argument is ever null!
    }
}
Run Code Online (Sandbox Code Playgroud)

在典型场景中,如果只是对具有此类列的表执行标准 CRUD,则databaseObject永远不可能null,因此这样的实现似乎就足够了。

然而,Converter API 的 Javadoc(现在)说:

无论上述值Converter的编码如何null,实现都必须能够处理null值。

这样的转换器可以通过简单地在每个方法中检查并返回 null 来实现 null 安全,如下所示:

public class StringCaseConverter extends org.jooq.impl.AbstractConverter<String, String> {
    public StringCaseConverter() {
        super(String.class, String.class);
    }

    @Override
    public String from(String databaseObject) {
        return databaseObject == null ? null : databaseObject.toLowerCase();
    }

    @Override
    public String to(String userObject) {
        return userObject == null ? null : userObject.toUpperCase();
    }
}
Run Code Online (Sandbox Code Playgroud)

或者,可以使用它Converter.ofNullable(String.class, String.class, String::toLowerCase, String::toUpperCase)来执行空检查。

但为什么这是必要的呢?在什么情况下该Converter.from(databaseObject)方法可能会接收并预期处理null

Luk*_*der 5

这个问题在概念上与为什么 jOOQ 不能通过类型系统保证可空性并没有真正的不同,自从 jOOQ 支持KotlinGenerator. 然而,它确实从一个有趣的新角度说明了这个问题。

\n

有关打字问题的讨论,请参阅问题 #13999

\n

NOT NULL列变为空的简单情况

\n

假设:

\n
CREATE TABLE a (i INT NOT NULL PRIMARY KEY);\nCREATE TABLE b (i INT REFERENCES a); -- Optional foreign key\n
Run Code Online (Sandbox Code Playgroud)\n

简单的查询案例

\n
// Explicit left join\nctx.select(B.I, A.I)\n   .from(B)\n   .leftJoin(A).on(B.I.eq(A.I))\n   .fetch();\n\n// Implicit left join\nctx.select(B.I, B.a().I)\n   .from(B)\n   .fetch();\n
Run Code Online (Sandbox Code Playgroud)\n

在这两种情况下,尽管在表定义中声明了它,A.I在查询结果中结果可以为空。NOT NULL

\n

同样,当使用显式UNION或隐式的,例如via GROUPING SETS(包括ROLLUPCUBE语法糖)时,我们得到相同的行为:

\n
ctx.select(A.I, count())\n   .from(A)\n   .groupBy(rollup(A.I))\n   .fetch();\n
Run Code Online (Sandbox Code Playgroud)\n

这只是语法糖:

\n
ctx.select(A.I, count())\n   .from(A)\n   .groupBy(A.I)\n   .unionAll(\n    select(inline(null, A.I.getDataType()), count())\n   .from(A)\n   .groupBy())\n   .fetch();\n
Run Code Online (Sandbox Code Playgroud)\n

当结果行出现时,不可能知道是第一UNION ALL个子查询还是第二个子查询产生了它(我们可以NULL在这种特殊情况下实现检查来识别子查询,但投影可能不可用,或者可能有其他这是不可行的原因)。

\n

简而言之,在 SQL 中,可以在一个上下文中注释为的NOT NULL表达式突然不能在另一个上下文中注释。这就是为什么可空性信息不能被认为是可信的,至少不能通过 Java 的类型系统。

\n

使可空性信息在运行时可用

\n

当然,即使 Java 编译器(或 kotlin / scala 编译器)无法强制执行,也可以在整个运行时类型表示中传播可空性。该领域已经做了一些工作,并且还会有更多工作:#11070。在某种程度上,这就是你要问的。您将该Converter实例附加到特定列,并且希望 jOOQ 将 SQL 的代数传播到您的Converter,避免NULL在它看起来是正确的事情时传递给它。

\n

但这“正确的事”是什么?我们之前已经看到,原来的相同表达式NOT NULL可以突然变成NULL。在 jOOQ 查询情况下,A.I仍然是一个简单的查询,但是、、和一些其他运算符NOT NULL的存在将改变查询中的情况。LEFT JOINUNIONGROUPING SETS

\n

即使 jOOQ 确实实现了巧妙的逻辑来以某种方式记住这一点(至少,在可能的情况下),它也不会是一半(?) jOOQ 用户想要的,而且它也不会总是有效。上面的ROLLUP查询产生一个:Result<Record2<Integer, Integer>>。当您将 a 附加Converter<Integer, MyType>A.ID列时,它将变成Result<Record2<MyType, Integer>>.

\n

您可以使用代码生成将其附加到ConverterA.I也可以使用 临时转换器Converter将其附加到A.I查询中:

\n
Result<Record2<MyType, Integer>> result =\nctx.select(A.I.convertFrom(new MyConverter()), count())\n   .from(A.I)\n   .fetch();\n
Run Code Online (Sandbox Code Playgroud)\n

或者,您可以将ResultQuery::coerce其附加Converter到内容甚至类型不安全的查询(例如纯 SQL 模板),这与 jOOQ 相同。

\n

当 jOOQRecord从底层 JDBC 获取 jOOQ 值时,有关该值如何形成的ResultSet信息就会“丢失”。Record特别是,该UNION案例表明不可能知道列现在是否可以为空。

\n
\n

作为旁注,该UNION案例还表明只有第一UNION个子查询转换器可用于获取。一个“有趣”的警告。

\n
\n

结论

\n

所以,自从:

\n
    \n
  1. 可空性很难(但并非不可能)在整个查询中传播
  2. \n
  3. 为空性是不可能从任意结果集得出的(感谢UNION等等)
  4. \n
  5. Converter是用于通用数据类型转换的通用 SPI T <-> U,与上下文无关。
  6. \n
  7. A可能决定对其类型Converter使用非空“对象”表示。NULLU
  8. \n
\n

您只需NULL每个实现中处理该情况即可Converter。jOOQ 无法代表您做出任何假设。

\n

这是 jOOQ 为可预测性、逻辑性、简单性而做出的权衡,而不是偶尔巧妙的“改进”(这将不可避免地产生非常奇怪的警告和边缘情况)

\n