jooq single query with one to many relationship - java

I have a table experiment and a table tags. There may be many tags for one experiment.
schema:
-------- --------
|Table1| 1 n |Table2|
| | <--------------> | |
| | | |
-------- --------
(experiment) (tags)
Is it possible to create a query with jooq which returns the experiments and the corresponding List of tags?
something like Result<Record> where Record is a experimentRecord and a list of Tags, or a map<experimentRecord, List<TagRecord>.
I also have a query which returns only one result, is there something convenient out there?
EDIT: java8, newest jooq.

There are many ways to materialise a nested collection with SQL, and / or with jOOQ. I'm just going through some of them:
Using joins
If you don't deeply nest those collections, denormalising (flattening) your results with a JOIN might do the trick for you, without adding too much overhead as data is being duplicated. Essentially, you'll write:
Map<ExperimentRecord, Result<Record>> map =
DSL.using(configuration)
.select()
.from(EXPERIMENT)
.join(TAGS)
.on(...)
.fetchGroups(EXPERIMENT);
The above map contains experiment records as keys, and nested collections containing all the tags as values.
Creating two queries
If you want to materialise a complex object graph, using joins might no longer be optimal. Instead, you probably want to collect the data in your client from two distinct queries:
Result<ExperimentRecord> experiments =
DSL.using(configuration)
.selectFrom(EXPERIMENT)
.fetch();
And
Result<TagsRecord> tags =
DSL.using(configuration)
.selectFrom(TAGS)
.where(... restrict to the previous experiments ...)
.fetch();
And now, merge the two results in your client's memory, e.g.
experiments.stream()
.map(e -> new ExperimentWithTags(
e,
tags.stream()
.filter(t -> e.getId().equals(t.getExperimentId()))
.collect(Collectors.toList())
));
Nesting collections using SQL/XML or SQL/JSON
This question didn't require it, but others may find this question in search for a way of nesting to-many relationships with jOOQ. I've provided an answer here. Starting with jOOQ 3.14, you can use your RDBMS's SQL/XML or SQL/JSON capabilities, and then use Jackson, Gson, or JAXB to nest collections like this:
List<Experiment> experiments =
ctx.select(
EXPERIMENT.asterisk(),
field(
select(jsonArrayAgg(jsonObject(TAGS.fields())))
.from(TAGS)
.where(TAGS.EXPERIMENT_ID.eq(EXPERIMENT.ID))
).as("tags")
)
.from(EXPERIMENT)
.fetchInto(Experiment.class);
Where Experiment is a custom Java class like this:
class Experiment {
long id;
String name;
List<Tag> tags;
}
class Tag {
long id;
String name;
}
Nesting collections using MULTISET
Even better than the above, you can hide using SQL/XML or SQL/JSON behind jOOQ 3.15's new MULTISET operator support. Assuming the above Java classes are Java 16 records (or any other immutable classes), you can even map nested collections type safely into your DTOs:
List<Experiment> experiments =
ctx.select(
EXPERIMENT.ID,
EXPERIMENT.NAME,
multiset(
select(TAGS.ID, TAGS.NAME)
.from(TAGS)
.where(TAGS.EXPERIMENT_ID.eq(EXPERIMENT.ID))
).as("tags").convertFrom(r -> r.map(Records.mapping(Tag::new)))
)
.from(EXPERIMENT)
.fetch(Records.mapping(Experiment::new));
Where Experiment is a custom Java class like this:
record Experiment(long id, String name, List<Tag> tags) {}
record Tag(long id, String name) {}
See also this blog post for more information.

You can now use SimpleFlatMapper to map your result to a Tuple2<ExperimentRecord, List<TagRecord>>. All you need to do is.
1 - create a mapper, specify the key column, assumed it would be id
JdbcMapper mapper =
JdbcMapperFactory
.newInstance()
.addKeys(EXPERIMENT.ID.getName())
.newMapper(new TypeReference<Tuple2<ExperimentRecord, List<TagRecord>>>() {});
2 - use the mapper on the ResultSet of your query
try (ResultSet rs = DSL.using(configuration)
.select()
.from(EXPERIMENT)
.join(TAGS)
.on(...)
.fetchResultSet()) {
Stream<Tuple2<ExperimentRecord, List<TagRecord>>> stream = mapper.stream(rs);
....
}
See here for more details

Related

Build pojo from multiple sql queries results using jooq

Usual way is to map a single query result to a class e.g.
List<MyPojo> p = ctx.fetch("select * from table...").into(MyPojo.class);
But what if I want to build a pojo based on several queries results? Note that some of pojo fields are objects.
class MyPojo{
String field1 - query1
int field2 - query1
List<SomeClass> field3 - query2
SomeClass2 - query3
...
}
Will really appreciate any thoughts.
You could obviously map your code manually by running several queries and assembling the results into your result type individually (there's no automatic way to do this in jOOQ). But much better than that, just use
jOOQ's code generator (things will get so much better with that!)
MULTISET, nested records, and ad-hoc converters
An example would be:
List<MyPojo> result =
ctx.select(
T.FIELD1,
T.FIELD2,
multiset(
select(U.COL1, U.COL2)
.from(U)
.where(U.T_ID.eq(T.ID))
).convertFrom(r -> r.map(Records.mapping(SomeClass::new))),
field(
select(row(X.COL1, X.COL2).mapping(SomeClass2::new))
.from(X)
.where(X.T_ID.eq(T.ID))
))
.from(T)
.fetch(Records.mapping(MyPojo::new));
All of this is completely type safe and not using any reflection. All you need for those constructor references to work is add the constructor to your MyPojo, or make it a record, instead:
record MyPojo(
String field1,
int field2,
List<SomeClass> field3,
SomeClass2 field4
) {}

JOOQ arrayAgg array_agg fields as object

I have 2 entities:
record Customer(String name, List<CustomerContact > contactHistory) {}
record CustomerContact(LocalDateTime contactAt, Contact.Type type) {
public enum Type {
TEXT_MESSAGE, EMAIL
}
}
These are persisted in a schema with 2 tables:
CREATE TABLE customer(
"id". BIGSERIAL PRIMARY KEY,
"name" TEXT NOT NULL
);
CREATE TABLE customer_contact(
"customer_id" BIGINT REFERENCES "customer" (ID) NOT NULL,
"type" TEXT NOT NULL,
"contact_at" TIMESTAMPTZ NOT NULL DEFAULT (now() AT TIME ZONE 'utc')
);
I want to retrieve the details of my Customers with a single query, and use the arrayAgg method to add the contactHistory to each customer. I have a query like this:
//pseudo code
DSL.select(field("customer.name"))
.select(arrayAgg(field("customer_contact.contact_at")) //TODO How to aggregate both fields into a CustomerContact object
.from(table("customer"))
.join(table("customer_contact")).on(field("customer_contact.customer_id").eq("customer.id"))
.groupBy(field("customer_contact.customer_id"))
.fetchOptional()
.map(asCustomer());
The problem I have with this is that arrayAgg will only work with a single field. I want to use 2 fields, and bind them into a single object (CustomerContact) then use that as the basis for the arrayAgg
Apologies if I have not explained this clearly! Any help much appreciated.
Rather than using ARRAY_AGG, how about using the much more powerful MULTISET_AGG or MULTISET to get the most out of jOOQ's type safe capabilities? Combine that with ad-hoc conversion for type safe mapping to your Java records, as shown also in this article. Your query would then look like this:
Using MULTISET_AGG
List<Customer> customers =
ctx.select(
CUSTOMER.NAME,
multisetAgg(CUSTOMER_CONTACT.CONTACT_AT, CUSTOMER_CONTACT.TYPE)
.convertFrom(r -> r.map(Records.mapping(CustomerContact::new))))
.from(CUSTOMER)
.join(CUSTOMER_CONTACT).on(CUSTOMER_CONTACT.CUSTOMER_ID.eq(CUSTOMER.ID))
.groupBy(CUSTOMER_CONTACT.CUSTOMER_ID)
.fetch(Records.mapping(Customer::new));
Note that the entire query type checks. If you change anything about the query or about your records, it won't compile anymore, giving you additional type safety. This is assuming that youre Type enum is either:
Generated from a PostgreSQL ENUM type
Converted automatically using an enum converter, attached to generated code
Depending on your tastes, using implicit joins could slightly simplify the query for you?
List<Customer> customers =
ctx.select(
CUSTOMER_CONTACT.customer().NAME,
multisetAgg(CUSTOMER_CONTACT.CONTACT_AT, CUSTOMER_CONTACT.TYPE)
.convertFrom(r -> r.map(Records.mapping(CustomerContact::new))))
.from(CUSTOMER_CONTACT)
.groupBy(CUSTOMER_CONTACT.CUSTOMER_ID)
.fetch(Records.mapping(Customer::new));
It's not a big deal in this query, but in a more complex query, it can reduce complexity.
Using MULTISET
An alterantive is to nest your query instead of aggregating, like this:
List<Customer> customers =
ctx.select(
CUSTOMER.NAME,
multiset(
select(CUSTOMER_CONTACT.CONTACT_AT, CUSTOMER_CONTACT.TYPE)
.from(CUSTOMER_CONTACT)
.where(CUSTOMER_CONTACT.CUSTOMER_ID.eq(CUSTOMER.ID))
).convertFrom(r -> r.map(Records.mapping(CustomerContact::new))))
.from(CUSTOMER)
.fetch(Records.mapping(Customer::new));
Code generation
For this answer, I was assuming you're using the code generator (you should!), as it would greatly contribute to this code being type safe, and make this answer more readable.
Much of the above can be done without code generation (except implicit joins), but I'm sure this answer could nicely demonstrate the benefits it terms of type safety.

Reuse RecordMapper in a jOOQ multiset query

Let's say we have a table car and parts. To fetch all car with their parts we use the following query:
#Transactional
public List<ReadCarDto> getAllCars() {
return getDslContext().select(
CAR.ID,
CAR.NAME,
CAR.DESCRIPTION,
multiset(
selectDistinct(
PARTS.ID,
PARTS.NAME,
PARTS.TYPE,
PARTS.DESCRIPTION
).from(PARTS).where(PARTS.CAR_ID.eq(CAR.ID))
).convertFrom(record -> record.map(record1 -> new ReadPartDto(
record1.value1(),
record1.value2(),
record1.value3(),
record1.value4()
)))
).from(CAR).fetch(record -> new ReadCarDto(
record.value1(),
record.value2(),
record.value3(),
record.value4()
));
}
Question: I always want to fetch the full car and part rows. Is there a way to reuse my existing private RecordMapper<CarRecord, ReadCarDto> getCarMapper() method that already implements the DTO conversion (For parts too of course)? Otherwise I have to retype the conversion in my multiset queries.
It looks like the selectDistinct method only has support for 1 - 22 fields and select().from(CAR) doesn't provide a multiset method.
Sidenote: I don't want to use the reflection conversion.
Your question reminds me of this one. You probably want to use a nested row() expression to produce nested records. Something like this:
return getDslContext()
.select(
row(
CAR.ID,
CAR.NAME,
CAR.DESCRIPTION,
...
).mapping(carMapper),
multiset(...)
)
A future jOOQ version will let you use CAR directly as a nested row in your projections, see https://github.com/jOOQ/jOOQ/issues/4727. As of jOOQ 3.16, this isn't available yet.

Hibernate: Fetching columns with their aliases

Consider this trivial query:
SELECT 1 as first, 2 as second
When using Hibernate we can then do something like:
em.createNativeQuery(query).fetchResultList()
However, there seem to be no way of getting the aliases (or column names). This would be very helpful for creating List<Map<String, Object>> where each map would be a row with their aliases, for instance in this case: [{first: 1, second: 2}].
Is there a way to do something like that?
I would suggest a bit different approach which may meet your needs.
In JPA 2.1 there is a feature called "result set mapping".
Basically you have to define a POJO class which would hold the result values (all the values must be passed using the constructor):
public class ResultClass{
private String fieldOne;
private String fieldTwo;
public ResultClass(String fieldOne, String fieldTwo){
this.fieldOne = fieldOne;
this.fieldTwo = fieldTwo;
}
}
Then you have to declare the mapping on one of your entities (does not matter on which, it just has to be a declated #Entity):
#SqlResultSetMapping(name="ResultMapping", classes = {
#ConstructorResult(targetClass = ResultClass.class,
columns = {#ColumnResult(name="columnOne"), #ColumnResult(name="columnTwo")})
})
The columnOne and columnTwo are aliases as declared in the select clause of the native query.
And finally use in the query creation:
List<ResultClass> results = em.createNativeQuery(query, "ResultMapping").getResultList();
In my opinion this is more elegant and "a level above" solution as you are not working with a generic Map key/values pairs but with a concrete POJO class.
You can use ResultTransformer interface . Implement custom mapper for mapping values with aliases.
here is example https://vladmihalcea.com/why-you-should-use-the-hibernate-resulttransformer-to-customize-result-set-mappings/
with ResultTransformer you can easy customize result set type , especially if you need aliases

Possible to query by key instead of value in Hazelcast (using Predicates)?

In Hazelcast, is it possible to query an IMap based on attributes of a key instead of the values? All the Hazelcast examples show querying by value. E.g., for a map of employees with keys that are strings:
IMap<String, Employee> employees;
The typical search predicates then search based on employee attributes (name, salary, etc). But my case uses more complex keys, such as:
IMap<DataAttributes, DataValue> myData;
So if DataAttributes has fields such as:
class DataAttributes {
String theDescription;
Date theStartTime;
public String getDescription() { return theDescription; }
// etc....
}
I want to write a predicate that can query by the keys, to return an appropriate DataValue object. This does not work:
Predicate pred = Predicates.equal("description", "myDescription");
myData.keySet(pred); // Throws IllegalArgumentException: "There is no suitable accessor for..."
I could roll-my-own as suggested in this answer, but I'd rather use an out-of-the-box solution if I can.
It doesn't matter if I wind up using the Criteria API, or the Distributed SQL Query API. Any working query would be great. Bonus points for a solution that works on nested attributes (i.e.: DataAttributes theStartTime.getYear()).
It is possible using PredicateBuilder (com.hazelcast.query.PredicateBuilder). The PredicateBuilder paradigm allows you to query based on keys, like so:
EntryObject eo = new PredicateBuilder().getEntryObject();
Predicate fredWithStartTimeThisYear = eo.key().get("Description").equal("Fred")
.and(eo.key().get("theStartTime.Year").equal(2015));
Note that you can refer to class members by accessor method ("getter") or field name, as you can see in the above example code. I found this information in the "Mastering Hazelcast" online book, available at hazelcast.org (but you have to fill out a registration form to gain access to it).

Categories