17

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.

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Leander
  • 1,322
  • 4
  • 16
  • 31

2 Answers2

19

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.

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • Thank you, i sometimes know i have only one result, is there a shortcut for that? – Leander Nov 25 '15 at 15:25
  • @Leander: I'm not sure what you mean... One result of what? `EXPERIMENT`? Or `TAGS`? – Lukas Eder Nov 25 '15 at 15:38
  • @Leander: I see. You can call `fetchOne()` or `fetchOptional()` rather than `fetch()` to get an individual record – Lukas Eder Nov 25 '15 at 22:07
  • But then there is no possiblity to grasp the TAGS collection? – Tom Feiner May 04 '16 at 14:06
  • @tfeiner: I'm sorry, I'm not sure if I understand what you mean by "grasping the tags collection"...? – Lukas Eder May 04 '16 at 14:22
  • I think what Leander means it: What if I am sure that I have exactly ONE experiment with a collection of tags? (so that Map> has exactly one entry) – Tom Feiner May 04 '16 at 14:24
  • OK, I see, but I would still just fetch one experiment with `fetchOne()`, and then collect the tags in a second query for that... Anyway, I'm very happy to answer a concrete question, i.e. a question where the target structure is well defined. There are dozens of solutions, I'd prefer not to be guessing what the desired target structure really is. – Lukas Eder May 04 '16 at 14:29
  • I was able to fetch the many relationships just using `arrayAgg` without the json object wrapper of the TAGs field. – Onome Sotu Jun 14 '22 at 23:09
  • @OnomeSotu: Yes, you can do that, but I suspect it's equivalent to the `MULTISET` or `MULTISET_AGG` approach, which is more convenient on the jOOQ side. – Lukas Eder Jun 15 '22 at 06:57
1

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

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
user3996996
  • 342
  • 3
  • 5