jOOQ - join with nested subquery - java

Let's say I want to find out who wrote CLRS in a book db (tables BOOK, AUTHOR with a junction table BOOK_AUTHOR).
SelectConditionStep<Record1<String>> query = create
.select(AUTHOR.LASTNAME.as("AuthorName"))
.from(
(
BOOK.leftOuterJoin(BOOK_AUTHOR).on(BOOK.ID.eq(BOOK_AUTHOR.BOOKID))
).leftOuterJoin(AUTHOR).on(AUTHOR.ID.eq(BOOK_AUTHOR.AUTHORID))
)
.where(BOOK.TITLE.eq(CLRS_title))
;
A bit inefficient to match the entire table, just to select a single book. I now want to select that book prior to the match.
The jOOQ doc on this matter led me to believe that could look something like this:
Table<Record1<Integer>> clrs = create
.select(BOOK.ID.as("bookID"))
.from(BOOK)
.where(BOOK.TITLE.eq(CLRS_title))
.asTable()
;
SelectJoinStep<Record1<String>> query = create
.select(AUTHOR.LASTNAME.as("AuthorName"))
.from(
(
clrs.leftOuterJoin(BOOK_AUTHOR).on(clrs.field("bookID").eq(BOOK_AUTHOR.BOOKID))
).leftOuterJoin(AUTHOR).on(AUTHOR.ID.eq(BOOK_AUTHOR.AUTHORID))
)
;
However, that fails to compile because
Cannot resolve method 'eq(org.jooq.TableField<ch.cypherk.bookdb.public_.tables.records.BookAuthorRecord,java.lang.Integer>)'
in the join condition.
What's the correct way to write this join?

The problem you're having
You're dereferencing a column from your derived table using Table.field(String):
clrs.field("bookID")
The type you're getting back is Field<?>, with a wildcard. Like with any generic type, once you have a wild card, a lot of operations (but not all) will no longer be possible on that type. Take List<?>, for example. You can still call List<?>.get() to retrieve an Object, but not List<?>.add(? element). In Field<?>, you can no longer call eq(), unless you cast the argument to a raw type.
You can also coerce your field's <T> type to the type you already know, e.g. by using Table.field(String, DataType<T>)
clrs.field("bookID", BOOK.ID.getDataType())
Study your various options and you might discover the one(s) you might find most useful
A better solution to your query
You don't really need to
Assign your subquery to a local variable
Use a derived table for your problem
Often with jOOQ, if you're having issues with derived tables as above, ask yourself is there really not an easier query I could write instead?
What you really need here is a semi join. Write:
// Assuming this static import
import static org.jooq.impl.DSL.*;
ctx.select(AUTHOR.LASTNAME)
.from(AUTHOR)
.where(AUTHOR.ID.in(
select(BOOK_AUTHOR.AUTHORID)
.from(BOOK_AUTHOR)
.join(BOOK).on(BOOK.ID.eq(BOOK_AUTHOR.BOOKID))
.where(BOOK.TITLE.eq(clrsTitle))
)
.fetch();

Related

jOOQ Subquery in Order By

I'm using a subquery in order by like this on MySQL 8 database:
select * from series
order by (select max(competition.competition_date) from competition
where competition.series_id = series.id) desc
But I didn't find a way to do that with jOOQ.
I tried the following query but this does not compile:
dsl
.selectFrom(SERIES)
.orderBy(dsl.select(DSL.max(COMPETITION.COMPETITION_DATE))
.from(COMPETITION).where(COMPETITION.SERIES_ID.eq(SERIES.ID)).desc())
.fetch()
Are subqueries not supported in order by?
Select<R> extends Field<R>
There's a pending feature request #4828 to let Select<R> extend Field<R>. This seems tempting because jOOQ already supports nested records to some extent for those dialects that support it.
But I have some doubts whether this is really a good idea in this case, because no database I'm aware of (i.e. where I tried this) supports scalar subqueries that project more than one column. It's possible to use such subqueries in row value expression predicates, e.g.
(a, b) IN (SELECT x, y FROM t)
But that's a different story, because it's limited to predicates, and not arbitrary column expressions. And it is already supported in jOOQ, via the various DSL.row() overloads, e.g.
row(A, B).in(select(T.X, T.Y).from(T))
Select<Record1<T>> extends Field<T>
This is definitely desireable, because a SELECT statement that projects only one column of type T really is a Field<T> in SQL, i.e. a scalar subquery. But letting Select<Record1<T>> extend Field<T> is not possible in Java. There is no way to express this using Java's generics. If we wanted to do this, we'd have to "overload" the Select type itself and create
Select1<T1> extends Select<Record1<T1>>
Select2<T1, T2> extends Select<Record2<T1, T2>>
etc.
In that case, Select1<T1> could be a special case, extending Field<T1>, and the other ones would not participate in such a type hierarchy. But in order to achieve this, we'd have to duplicate the entire Select DSL API per projection degree, i.e. copy it 22 times, which is probably not worth it. There are already 67 Select.*Step types in the jOOQ API, as of jOOQ 3.13. This makes it difficult to justify the enhancement even only for scalar subqueries, i.e. for Select1.
Using DSL.field(Select<Record1<T>>) and related API
You've already found the right answer. While Select<Record1<T>> cannot extend Field<T>, we can accept Select<? extends Record1<T>> in plenty of API, as an overload to the usual T|Field<T> overloads. This has been done occasionally, and might be done more thoroughly throughout the API: https://github.com/jOOQ/jOOQ/issues/7240.
It wouldn't help you, because you want to call .desc() on a column expression (the Select), rather than wrap pass it to a method, so we're back at Java's limitation mentioned before.
Kotlin and other languages
If you're using Kotlin or other languages that have some way of providing "extension functions", however, you could use this approach:
inline fun <T> Select<Record1<T>>.desc(): SortField<T> {
return DSL.field(this).desc();
}
jOOQ might provide these out of the box in the future: https://github.com/jOOQ/jOOQ/issues/6256
Turning the subquery into a Field works:
dsl.selectFrom(SERIES)
.orderBy(DSL.field(dsl.select(DSL.max(COMPETITION.COMPETITION_DATE)).from(COMPETITION)
.where(COMPETITION.SERIES_ID.eq(SERIES.ID))).desc())
.fetch()

JOOQ query to JOIN ON WITH clause

How can I write a JOOQ query to join on a field from a "with" clause?
For example, I've tried:
create.with("a").as(select(
val(1).as("x"),
val("a").as("y")
))
.select()
.from(tableByName("a")
.join(ANOTHER_TABLE)
.on(ANOTHER_TABLE.ID.eq(tableByName("a").field("x")))
.fetch();
However, as the compiler doesn't know the type of tableByName("a").field("x") it cannot resolve which eq() method to use. Given that I know the type, is there a way I can provide it explicitly? Or is there another approach I should take to join on a field from a "with" clause?
While I certainly agree with flutter's answer being a more desireable path to a solution here, I'll just quickly add a response that answers your specific compilation error question.
There are three things that are wrong with your current join predicate:
ANOTHER_TABLE.ID.eq(tableByName("a").field("x"))
DSL.tableByName() is deprecated. It is generally recommended to use table(Name) instead.
Such a dynamically constructed Table does not know of any of its field() references, thus table(name("a")).field("x") will return null
The compilation error is due to your ID reference being of type Field<Integer> (probably), and thus the Field.eq() method expects a Field<Integer> argument as well. Without any knowledge about the type of your field "x", the jOOQ API / Java compiler infers Field<Object>, which is invalid.
So, the solution would be to write:
// field(Name, Class)
ANOTHER_TABLE.ID.eq(field(name("a", "x"), Integer.class))
// field(Name, DataType)
ANOTHER_TABLE.ID.eq(field(name("a", "x"), ANOTHER_TABLE.ID.getDataType()))
I.e. to use DSL.field(Name, Class<T>), or DSL.field(Name, DataType<T>) if you're using custom data type bindings / converters.
What about declaring the CTE first?
Explicit common table expressions
CommonTableExpression<Record2<Integer, String>> a =
name("a").fields("x", "y").as(select(val(1), val("a")));
create.with(a)
.select()
.from(a)
.join(ANOTHER_TABLE)
.on(ANOTHER_TABLE.ID.eq(a.field("x")))
.fetch();
If this does not work, you can always get the DataType<?> or the Class<?> via the Field, which you can get via the Table.

What is fieldIndex about in JOOQ

I read JOOQ API documentation about following APIs, but still couldn't understand what is fieldIndex about.
fetchAny(int fieldIndex, java.lang.Class<? extends T> type)
For example, in the following code I already knows which column to select, why do we need filedIndex of 0? What does 0 mean?
String name = getDslContext().select(TESTB.STU_NAME)
.from(TESTB)
.where(TESTB.ID.eq(studentId))
.fetchAny(0, String.class);
jOOQ mostly operates on Record types. For instance, when you create the following query:
.select(TESTB.STU_NAME)
.from(TESTB)
.where(TESTB.ID.eq(studentId));
You're really operating on a ResultQuery<Record1<String>>. By calling fetchAny(0, String.class) on that type, you're telling jOOQ that you want to fetch any record, and from that record, you want to get only the value at index 0, converted to String.
This may feel like repeating the same information twice (using TESTB.STU_NAME, and fetching the column at index 0). This is because fetchign a single value is quite a special case in the jOOQ API. Unfortunately, the ResultQuery API doesn't really "know" that you're selecting only one column even if that type information is present via generics.
One alternative would be to use DSLContext.fetchValue(ResultQuery):
String name = getDslContext().fetchValue(
DSL.select(TESTB.STU_NAME)
.from(TESTB)
.where(TESTB.ID.eq(studentId))
);

Retrieving the value of selectCount in jooq

I have some code that looks like this:
Record record = jooq
.selectCount()
.from(USERS)
.fetchOne();
Currently I'm doing the following to get the count:
Integer count = (Integer) record.getValue(0);
But it seems like there must be a better solution (that's type-safe...since that's the whole point of using jooq). Any suggestions?
Unfortunately, for this particular query, there aren't many "better" ways to typesafely get the count() value. What you could do, to add type-safety, is this:
Field<Integer> f = count();
Integer count = jooq.
.select(f) // Or selectCount(). Replaced it to illustrate the case
.from(USERS)
.fetchOne(f);
The problem is that most of the type information about the projection has been "lost" to the Java compiler, by the time the fetch() methods are "reached". There is no way that a ResultQuery.fetchXXX() method could recover it from the SELECT clause, and produce it to you.
On the jOOQ user group, some users have argued to move the projection into the fetch() methods, entirely, the way C#'s LINQ, or Scala's SLICK do it. This would greatly complicate the expression of more advanced SELECT statements. An more elaborate explanation is documented here.
With jOOQ 3.0, additional record-level typesafety has been introduced. In jOOQ 3.3, it will thus be possible to fetch a single value as such (has been registered as #2246):
<T> T fetchValue(Select<Record1<T>> select);

Flatten a collection as part of an HQL query?

I have a requirement to use a reporting-friendly query from HQL. That is, each column is represented by a standard Java type (Long, Integer, String, List) rather than a mapped class. The values will be used by a third party library, so I have very limited control over post-processing.
My example object tree looks like this:
a.x
a.y (a collection of z)
a.y.z[0].v
a.y.z[1].v
a.y.z[2].v
I would like query to retrieve two columns. The first column is the plain "a.x" field, and the second is a String of a comma separated list of all of the a.y.z.v values. If this is not possible, then having the second column return as a Java list of the a.y.z.v values would be satisfactory.
In short, I would like to flatten the a.y.z.v collection to a csv String or to a List object from inside the query.
I have already attempted the following:
Using the "new" keyword in a list subselect. ie, "select a.x, (select new list(a.y.z.v)) from a". If necessary I could have transformed the contents of the list into a csv, but this caused a syntax error.
Using the "new" keyword with a custom object in a subselect. ie, "select a.x, (select new custom.package.ListToCsvObject(a.y.z)) from a". This caused the same error as the first attempt
Using the "elements()" keyword in the select. Unfortunately, this keyword only seems to work inside "in", "exists" clauses (etc), not as the actual returned value.
The only solution we've been able to find was to create a stored procedure in the database and use that, but such a solution is painfully slow through HQL (it turns a sub-second query into a 30 second query) and therefore is not something we want to continue to do.
I am able to make some limited changes to the Hibernate mapping (so I can add #formula, etc) but I would prefer not to have to make major changes to the database schema to support it. (So no, I don't want to create a denormalized "csv_value" column in the database!)
Could anyone suggest some code or, failing that, an alternative approach to solving this problem?
Try something like this should work. Flattening of list to comma separated string is done in the constructor of your VO class. You can also take a look at resultTransformer, you can create a custom resultTransformer and attach it to the query.
class ResultVO {
String x;
String y;
public ResultVO(String x,List<Z> y) {
this.x = x;
this.y = createCSV(y);
}
}
then in HQL
select new ResultVO(a.x,a.y) from a
A warning - this is not a good way to use JPA. If most of your use cases are like this you should seriously reconsider using some other persistence approach (ibastis, spring jdbc template + sql etc).

Categories

Resources