7

I am trying out JOOQ and trying to select from 3 tables (Author, Books and Articles) using a join statement. The ERD is as follows:

Author ----< Books
   |
   |
   ^
Articles

The query I have is the following:

    final List<Tuple3<AuthorRecord, BooksRecord, ArticlesRecord>> tupleList =
        persistenceContext.getDslContext()
            .select()
            .from(Author.AUTHOR)
            .join(Books.BOOKS)
            .on(Author.AUTHOR.ID.eq(Books.BOOKS.AUTHOR_ID))
            .join(Articles.ARTICLES)
            .on(Author.AUTHOR.ID.eq(Articles.ARTICLES.AUTHOR_ID))
            .where(Author.AUTHOR.ID.eq(id))
            .fetch()
            .map(r -> Tuple.tuple(r.into(Author.AUTHOR).into(AuthorRecord.class),
                r.into(Books.BOOKS).into(BooksRecord.class),
                r.into(Articles.ARTICLES).into(ArticlesRecord.class)));

I also have a protobuf object as follows:

message Author {
    int64 id = 1;
    string name = 2;
    repeated string books = 3;
    repeated string articles = 4;
}

(or any other pojo for that matter) which will hold all the entities (author details + list of books + list of articles) into one object. My question is, is there some way to map out of the box all three tables into one object using JOOQ.

Thanks in advance.

  • Can you describe the desired result type a bit more (I don't know protobuf). Is `repeated string` like a comma-separated, concatenated string like `"book1, book2, ..., bookN"`? Or is it more like an array? – Lukas Eder Mar 08 '17 at 19:45
  • Thanks @LukasEder the author class generated from protobuf would have getters to a list of books/articles. You can have a look [here](https://developers.google.com/protocol-buffers/docs/javatutorial) if you like to know more about protobuf. So in the ideal case the join query would give me a ProtobufAuthor or List In reality we ended up doing something as you suggested below. Thank you very much for your help! – armand.sciberras Mar 09 '17 at 06:03
  • Thanks for the feedback. Nice to know. Would you mind showing what you did in your own answer? (It's perfectly fine to answer one's own questions on Stack Overflow). I'd be very curious to see how you did it. – Lukas Eder Mar 09 '17 at 22:18

1 Answers1

7

Using JOIN doesn't work for this.

Your query will be rather inefficient because if you're using joins this way, you're creating a cartesian product between the books and the articles table, resulting in quite some memory and CPU consumption both in the database and in your Java client, before you de-duplicate all the meaningless combinations.

The "correct" SQL approach would be to use MULTISET as described in this article here. Unfortunately, jOOQ 3.9 doesn't support MULTISET yet (nor do many databases). So, you should create two separate queries:

  1. Fetching all the books
  2. Fetching all the articles

And then use something like Java 8 Streams to map them into a single object.

Using MULTISET starting from jOOQ 3.15

Luckily, starting from jOOQ 3.15, there's an out-of-the-box solution to nesting collections in SQL using MULTISET. Your query would look like this:

Using reflection

List<Author> authors =
ctx.select(
      AUTHOR.ID,
      AUTHOR.NAME,
      multiset(
        select(BOOKS.TITLE)
        .from(BOOKS)
        .where(BOOKS.AUTHOR_ID.eq(AUTHOR.ID))
      ).as("books"),
      multiset(
        select(ARTICLES.TITLE)
        .from(ARTICLES)
        .where(ARTICLES.AUTHOR_ID.eq(AUTHOR.ID))
      ).as("articles")
    )
   .from(AUTHOR)
   .where(AUTHOR.ID.eq(id))
   .fetchInto(Author.class);

Using type safe, ad-hoc conversion

List<Author> authors =
ctx.select(
      AUTHOR.ID,
      AUTHOR.NAME,
      multiset(
        select(BOOKS.TITLE)
        .from(BOOKS)
        .where(BOOKS.AUTHOR_ID.eq(AUTHOR.ID))
      ).as("books").convertFrom(r -> r.map(Record1::value1)),
      multiset(
        select(ARTICLES.TITLE)
        .from(ARTICLES)
        .where(ARTICLES.AUTHOR_ID.eq(AUTHOR.ID))
      ).as("articles").convertFrom(r -> r.map(Record1::value1))
    )
   .from(AUTHOR)
   .where(AUTHOR.ID.eq(id))
   .fetch(Records.mapping(Author::new));

For more information about MULTISET, please refer to this blog post, or the manual sections:

Using SQL/XML or SQL/JSON starting from jOOQ 3.14

Starting from jOOQ 3.14, you can nest collections via SQL/XML or SQL/JSON, if your RDBMS supports that. You can produce a document, and then use something like Gson, Jackson, or JAXB to map it back to your Java classes. For example:

List<Author> authors =
ctx.select(
      AUTHOR.ID,
      AUTHOR.NAME,
      field(
        select(jsonArrayAgg(BOOKS.TITLE))
        .from(BOOKS)
        .where(BOOKS.AUTHOR_ID.eq(AUTHOR.ID))
      ).as("books"),
      field(
        select(jsonArrayAgg(ARTICLES.TITLE))
        .from(ARTICLES)
        .where(ARTICLES.AUTHOR_ID.eq(AUTHOR.ID))
      ).as("articles")
    )
   .from(AUTHOR)
   .where(AUTHOR.ID.eq(id))
   .fetchInto(Author.class);

Note that JSON_ARRAYAGG() aggregates empty sets into NULL, not into an empty []. If that's a problem, use COALESCE()

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • Hi @Lukas, what is the way to consume the `MULTISET`? Ideally, type-safely. I see in the returned type that the column is added to `Result` tuple as a nested `Result`. Although, I only see `get(String)` as a way to get to the column, and that returns just `Object`. Could you please link some example? – Ondra Žižka Jun 05 '23 at 14:25
  • I found out that if I keep the types along, I can call `result.value11()` and that is the nested `Result`. Is there any better way? Like, `result.getSubResult()`? – Ondra Žižka Jun 05 '23 at 15:00
  • @OndraŽižka: Can you please ask a new, specific question here on Stack Overflow, with details about what your doubts are? – Lukas Eder Jun 05 '23 at 17:58