In jOOQ code generation, it is possible to assign a converter to a NOT NULL field like so:
<forcedType>
<includeTypes>(?i)^varchar\(\d+\)$</includeTypes>
<userType>String</userType>
<nullability>NOT_NULL</nullability><!-- Converter applies only to NOT NULL columns! -->
<converter>StringCaseConverter</converter>
</forcedType>
And then a Converter may be implemented like so:
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!
}
}
In typical scenarios, where one is simply performing standard CRUD on a table with such a column, then the databaseObject can never be null, so such an implementation would appear to suffice.
However the Javadoc for the Converter API (now) says:
Irrespective of the
Converter's encoding ofnullvalues above, an implementation must be able to handlenullvalues.
Such a Converter may be made null-safe by simply checking and returning null in each method like so:
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();
}
}
Alternatively one can use Converter.ofNullable(String.class, String.class, String::toLowerCase, String::toUpperCase) to perform the null checks.
But why is this necessary? Under what scenarios might the Converter.from(databaseObject) method receive and be expected to handle null?
This question isn't really conceptually different from why can't nullability be guaranteed by jOOQ via the type system, a question that has seen much interest by the kotlin community ever since jOOQ supported a
KotlinGenerator. However, it does illustrate the problem from an interesting new angle.For a discussion about the typing question, see issue #13999.
Simple cases where
NOT NULLcolumns become nullAssuming:
The simple case of querying
In both of these cases,
A.Iturns out to be nullable in the query result, despite it being declaredNOT NULLin the table definition.Likewise, when using explicit
UNIONor implicit ones, e.g. viaGROUPING SETS(including theROLLUPorCUBEsyntax sugars), we get the same behaviour:This is just syntax sugar for:
When the resulting row appears, it's impossible to know whether the first
UNION ALLsubquery or the second one produced it (we could implementNULLchecks in this particular case to identify the subquery, but the projection might not be available, or there may be other reasons why this isn't viable).In short, in SQL, an expression that can be annotated as
NOT NULLin one context suddenly cannot in another. This is why nullability information cannot be assumed trustworthy, at least not via Java's type system.Making nullability information available at runtime
Of course, it would be possible to propagate nullability throughout the runtime type representation, even if the Java compiler (or kotlin / scala compilers) cannot enforce it. There has been some work in that area, and there will be more: #11070. In a way, that's what you're asking. You're attaching that
Converterinstance to a specific column, and you would like jOOQ to propagate SQL's algebra to yourConverter, avoiding ever passingNULLto it when it appears to be the Right Thing™ to do.But what is this "Right Thing?" We've seen before that the same expression that is originally
NOT NULLcan suddenly becomeNULL. In the jOOQ query case,A.Iis stillNOT NULLin a trivial query, but the presence ofLEFT JOIN,UNION,GROUPING SETS, and a few other operators will change that within the query.Even if jOOQ did implement clever logic to somehow remember this (at least, when it is possible), it would not be what half (?) of jOOQ's users want, and it wouldn't always work. The above
ROLLUPquery produces a:Result<Record2<Integer, Integer>>. And when you attach aConverter<Integer, MyType>to yourA.IDcolumn, it will become aResult<Record2<MyType, Integer>>.You can attach that
ConvertertoA.Iusing code generation, or you can attach the sameConvertertoA.Iwithin the query using ad-hoc converters:Or, you could use
ResultQuery::coerceto attach that sameConverterto a query whose contents aren't even type safe (e.g. a plain SQL template), it's all the same to jOOQ.At the time when jOOQ fetches jOOQ
Recordvalues from the underlying JDBCResultSet, the information about how theRecordmay have come to be is "lost." In particular, theUNIONcase shows that it is impossible to know whether that column is now nullable or not.Conclusion
So, since:
UNIONetc.)Converteris a generic SPI for genericT <-> Udata type conversions, irrespective of context.Convertermay decide to use a non-null "NULLobject" representation for itsUtype.You simply have to handle the
NULLcase in each and everyConverterimplementation. jOOQ can't make any assumptions on your behalf, here.It's a tradeoff jOOQ made in favour of predictability, logic, simplicity, against the occasional clever "improvement" (which would inevitably turn out to produce very weird caveats and edge cases)