6

I am struggling to understand how to handle pojos with one-to-many and many-to-many relationships with JOOQ.

I store locations that are created by players (one-to-many relation). A location can hold multiple additional players who may visit it (many-to-many). The database layout comes down to the following:

CREATE TABLE IF NOT EXISTS `Player` (
  `player-id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `player` BINARY(16) NOT NULL,
  PRIMARY KEY (`player-id`),
  UNIQUE INDEX `U_player` (`player` ASC))
ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS `Location` (
  `location-id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(32) CHARACTER SET 'utf8' COLLATE 'utf8_bin' NOT NULL,
  `player-id` INT UNSIGNED NOT NULL COMMENT '
  UNIQUE INDEX `U_name` (`name` ASC),
  PRIMARY KEY (`location-id`),
  INDEX `Location_Player_fk` (`player-id` ASC),
  CONSTRAINT `fk_location_players1`
    FOREIGN KEY (`player-id`)
    REFERENCES `Player` (`player-id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
ENGINE = InnoDB;

CREATE TABLE IF NOT EXISTS `location2player` (
  `location-id` INT UNSIGNED NOT NULL,
  `player-id` INT UNSIGNED NOT NULL,
  INDEX `fk_ location2player_Location1_idx` (`location-id` ASC),
  INDEX `fk_location2player_Player1_idx` (`player-id` ASC),
  CONSTRAINT `fk_location2player_Location1`
    FOREIGN KEY (`location-id`)
    REFERENCES `Location` (`location-id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
  CONSTRAINT `fk_location2player_Player1`
    FOREIGN KEY (`player-id`)
    REFERENCES `Player` (`player-id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB;

Within my java application, all these informations are stored within one pojo. Note that the player and the list of invited players can be updated from within the application and need to be updated in the database as well:

public class Location {

    private final String name;
    private UUID player;
    private List<UUID> invitedPlayers;

    public void setPlayer(UUID player) {
        this.player = player;
    }

    public void invitePlayer(UUID player) {
        invitedPlayers.add(player);
    }

    public void uninvitePlayer(UUID player) {
        invitedPlayers.remove(player);
    }

    //additional methods…
}

Can I use JOOQ’s pojo mapping to map these three records into the single pojo? Can I use JOOQ’s CRUD feature from this pojo to update the one-to-many and many-to-many relations? If the pojo mapping cannot be used, can I take advantage of JOOQ in any way except using it to write my SQL statements?

Community
  • 1
  • 1
thee
  • 510
  • 1
  • 7
  • 20

2 Answers2

11

Using MULTISET for nested collections with jOOQ 3.15

Starting from jOOQ 3.15, you can use the standard SQL MULTISET operator to nest collections, and to abstract over the below SQL/XML or SQL/JSON serialisation format. Your query would look like this:

List<Location> locations
ctx.select(
      LOCATION.NAME,
      LOCATION.PLAYER,
      multiset(
        select(LOCATION2PLAYER.PLAYER_ID)
        .from(LOCATION2PLAYER)
        .where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
      ).as("invitedPlayers")
    )
   .from(LOCATION)
   .fetchInto(Location.class);

If your DTOs were immutable (e.g. Java 16 records), you can even avoid using reflection for mapping, and map type safely into your DTO constructors using constructor references and the new jOOQ 3.15 ad-hoc conversion feature.

List<Location> locations
ctx.select(
      LOCATION.NAME,
      LOCATION.PLAYER,
      multiset(
        select(LOCATION2PLAYER.PLAYER_ID)
        .from(LOCATION2PLAYER)
        .where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
      ).as("invitedPlayers").convertFrom(r -> r.map(Record1::value1))
    )
   .from(LOCATION)
   .fetch(Records.mapping(Location::new));

See also this blog post for more details about MULTISET

Using SQL/XML or SQL/JSON for nested collections with jOOQ 3.14

Starting from jOOQ 3.14, it's possible to nest collections using SQL/XML or SQL/JSON, if your RDBMS supports that. You can then use Jackson, Gson, or JAXB to map from the text format back to your Java classes. For example:

List<Location> locations
ctx.select(
      LOCATION.NAME,
      LOCATION.PLAYER,
      field(
        select(jsonArrayAgg(LOCATION2PLAYER.PLAYER_ID))
        .from(LOCATION2PLAYER)
        .where(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
      ).as("invitedPlayers")
        .convertFrom(r -> r.map(Records.mapping(Pla)
    )
   .from(LOCATION)
   .fetch(Records.mapping(Location::new));

In some database products, like PostgreSQL, you could even use SQL array types using ARRAY_AGG() and skip using the intermediate XML or JSON format.

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

Historic answer (pre jOOQ 3.14)

jOOQ doesn't do this kind of POJO mapping out of the box yet, but you can leverage something like ModelMapper which features a dedicated jOOQ integration, which works for these scenarios to a certain extent.

Essentially, ModelMapper hooks into jOOQ's RecordMapper API. More details here:

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • Thanks. But how would I fetch the many-to-many data into JOOQ's logic? In JDBC I would fetch the data with `JOIN` clauses and add each additional row to a collection in the pojo, but with JOOQ the RecordMapper cannot access previous objects. Am I expected to execute another query for the many-to-many relation right in the `RecordMapper` when creating the corresponding object? Or is there another way to do this? – thee May 25 '14 at 18:16
  • 1
    Yes, `RecordMapper` is a functional interface, so it isn't easy to implement something "stateful" as a grouping algorithm. But we also have various [`Result.intoGroups()`](http://www.jooq.org/javadoc/latest/org/jooq/Result.html#intoGroups(org.jooq.Field[])). Maybe, these will be helpful? Another option would be to leverage something like [ModelMapper](http://modelmapper.org/user-manual/jooq-integration/), which natively supports jOOQ. In any case, you should never execute two queries just because the ResultSet to Object mapping algorithm is getting a little complex. – Lukas Eder May 26 '14 at 06:13
  • In other [answers](http://stackoverflow.com/questions/33845239/jooq-single-query-with-one-to-many-relationship) (as well as on your [blog](https://blog.jooq.org/2014/07/11/java-8-friday-more-functional-relational-transformation/)) you have mentioned that Java 8's streams can be useful to properly match complex relationships from the database into POJOs. From this perspective, can Streams be used to solve the problem of this question? – thee Jun 20 '16 at 20:41
  • @thee: Yes, they can, of course. Or even easier than with Streams, you could use jOOλ. My answer here isn't exhaustive. I'll add some more examples in the near future, thanks for pointing this out. – Lukas Eder Jun 21 '16 at 11:48
  • Any news? I am currently re-evaluating this problem and some detailed examples would massively help me as I am currently not sure whether I should stick with JOOQ or switch to other solutions. – thee Oct 27 '16 at 11:38
  • 1
    @thee: Would you be interested in stating your use-case more in detail on the user group? https://groups.google.com/forum/#!forum/jooq-user. If I know your specific expectations, I can much better answer your questions perhaps, than here. Of course, you can do these mappings with the tools I've mentioned, but it's hard to say (and discuss on Stack Overflow), whether that's good enough for you. – Lukas Eder Oct 27 '16 at 12:05
3

You can use SimpleFlatMapper on the ResultSet of the query.

create a mapper with player as the key

JdbcMapper<Location> jdbcMapper = 
    JdbcMapperFactory.addKeys("player").newMapper(Location.class);

Then use fetchResultSet to get the ResultSet and pass it to the mapper. Note that it is important to orderBy(LOCATION.PLAYER_ID) otherwise you might end up with split Locations.

try (ResultSet rs = 
    dsl
        .select(
                LOCATION.NAME.as("name"), 
                LOCATION.PLAYER_ID.as("player"), 
                LOCATION2PLAYER.PLAYERID.as("invited_players_player"))
        .from(LOCATION)
            .leftOuterJoin(LOCATION2PLAYER)
                .on(LOCATION2PLAYER.LOCATION_ID.eq(LOCATION.LOCATION_ID))
        .orderBy(LOCATION.PLAYER_ID)
        .fetchResultSet()) { 
    Stream<Location> stream = jdbcMapper.stream(rs);

}

then do what you need to do on the stream, you can also get an iterator.

user3996996
  • 342
  • 3
  • 5
  • It's important to note that this solution bypassed the Jooq's record mapper and goes straight to the result set. This is problematic if you have any Jooq force types - you will have to redefine them for SFM – Amit Goldstein Dec 09 '18 at 10:02
  • My understanding is that the forced type is for the generation of Record object. you can use sfm to map against jOOQ record object using the forced type. The problem then is how to you map the join with the Record object? you will need to use a Tuple2> – user3996996 Dec 10 '18 at 11:14
  • I went around the forced type issue by creating a ConverterFactoryProducer and creating converters by going through the list of jooq converters and passing their methods to constantConverter(). If you are interested I will add a gist. Haven't got a chance to test it thoroughly though. – Amit Goldstein Dec 12 '18 at 09:49
  • interesting would you mind opening a ticket a put a bit more detail, I don't think I actually understand the problem – user3996996 Dec 13 '18 at 17:09