Introduction

In jOOQ you can generate POJOs as Java records by enabling the pojosAsJavaRecordClasses flag, but this can lead to some interesting behaviors, one of which I would like to describe here.

The case

Let’s say we have the following table in our database:

CREATE TABLE "user" (
   id uuid NOT NULL PRIMARY KEY,
   "name" varchar NOT NULL,
   status varchar NOT NULL,
   created_on timestamp NOT NULL
);

And we would like to build a query in jOOQ that retrieves the most recently created user within each status group. The equivalent SQL query is:

WITH "ranked_users_by_status" AS
  (SELECT id,
          "name",
          status,
          created_on,
          ROW_NUMBER() OVER (PARTITION BY status
                             ORDER BY created_on DESC, id DESC) AS "rank"
   FROM "user")
SELECT *
FROM "ranked_users_by_status"
WHERE "rank" = 1

Basically, in the WITH block, we build a query that retrieves users grouped by their status. Within each group, the users are sorted by creation date and id. A sequential row number (rank) is then assigned to each user within their respective status group, reflecting the sorted order. Finally, we select the users with the highest rank (the most recently created).

Query implementation in jOOQ

Assuming we already have the corresponding classes generated (including the POJO), the query implementation will be:

public List<User> findMostRecentlyCreatedUsersByStatusGroups(DSLContext dslContext) {
  Field<Integer> rankField =
      rowNumber()
          .over(partitionBy(USER.STATUS).orderBy(USER.CREATED_ON.desc(), USER.ID.desc()))
          .as("rank");

  return dslContext
      .with("ranked_users_by_status")
      .as(select(USER.fields()).select(rankField).from(USER))
      .select()
      .from(table(name("ranked_users_by_status")))
      .where(rankField.eq(1))
      .fetchInto(User.class);
}

Problem

If we generate the jOOQ POJO with the pojosAsJavaRecordClasses flag unspecified or set to false, and run the jOOQ query mentioned above, we will successfully retrieve the corresponding User objects that match the condition.

But if we generate the POJO with pojosAsJavaRecordClasses = true (i.e., the generated POJO will be a java record) and run the jOOQ query, we will encounter the following exception:

Exception in thread "main" org.jooq.exception.MappingException: No DefaultRecordMapper strategy applies to 
type class jooq.generated.tables.pojos.User for row type 

Below, you can find both generated POJOs:

User.java
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;


/**
* This class is generated by jOOQ.
  */
  @SuppressWarnings({ "all", "unchecked", "rawtypes" })
  public class User implements Serializable {

  private static final long serialVersionUID = 1L;

  private UUID id;
  private String name;
  private String status;
  private LocalDateTime createdOn;

  public User() {}

  public User(User value) {
  this.id = value.id;
  this.name = value.name;
  this.status = value.status;
  this.createdOn = value.createdOn;
  }

  public User(
  UUID id,
  String name,
  String status,
  LocalDateTime createdOn
  ) {
  this.id = id;
  this.name = name;
  this.status = status;
  this.createdOn = createdOn;
  }

  /**
  * Getter for <code>public.user.id</code>.
    */
    public UUID getId() {
    return this.id;
    }

  /**
  * Setter for <code>public.user.id</code>.
    */
    public void setId(UUID id) {
    this.id = id;
    }

  /**
  * Getter for <code>public.user.name</code>.
    */
    public String getName() {
    return this.name;
    }

  /**
  * Setter for <code>public.user.name</code>.
    */
    public void setName(String name) {
    this.name = name;
    }

  /**
  * Getter for <code>public.user.status</code>.
    */
    public String getStatus() {
    return this.status;
    }

  /**
  * Setter for <code>public.user.status</code>.
    */
    public void setStatus(String status) {
    this.status = status;
    }

  /**
  * Getter for <code>public.user.created_on</code>.
    */
    public LocalDateTime getCreatedOn() {
    return this.createdOn;
    }

  /**
  * Setter for <code>public.user.created_on</code>.
    */
    public void setCreatedOn(LocalDateTime createdOn) {
    this.createdOn = createdOn;
    }

  @Override
  public boolean equals(Object obj) {
  if (this == obj)
  return true;
  if (obj == null)
  return false;
  if (getClass() != obj.getClass())
  return false;
  final User other = (User) obj;
  if (this.id == null) {
  if (other.id != null)
  return false;
  }
  else if (!this.id.equals(other.id))
  return false;
  if (this.name == null) {
  if (other.name != null)
  return false;
  }
  else if (!this.name.equals(other.name))
  return false;
  if (this.status == null) {
  if (other.status != null)
  return false;
  }
  else if (!this.status.equals(other.status))
  return false;
  if (this.createdOn == null) {
  if (other.createdOn != null)
  return false;
  }
  else if (!this.createdOn.equals(other.createdOn))
  return false;
  return true;
  }

  @Override
  public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
  result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
  result = prime * result + ((this.status == null) ? 0 : this.status.hashCode());
  result = prime * result + ((this.createdOn == null) ? 0 : this.createdOn.hashCode());
  return result;
  }

  @Override
  public String toString() {
  StringBuilder sb = new StringBuilder("User (");

       sb.append(id);
       sb.append(", ").append(name);
       sb.append(", ").append(status);
       sb.append(", ").append(createdOn);

       sb.append(")");
       return sb.toString();
  }
  }
  
User.java (Record)
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;


/**
* This class is generated by jOOQ.
  */
  @SuppressWarnings({ "all", "unchecked", "rawtypes" })
  public record User(
  UUID id,
  String name,
  String status,
  LocalDateTime createdOn
  ) implements Serializable {

  private static final long serialVersionUID = 1L;


    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final User other = (User) obj;
        if (this.id == null) {
            if (other.id != null)
                return false;
        }
        else if (!this.id.equals(other.id))
            return false;
        if (this.name == null) {
            if (other.name != null)
                return false;
        }
        else if (!this.name.equals(other.name))
            return false;
        if (this.status == null) {
            if (other.status != null)
                return false;
        }
        else if (!this.status.equals(other.status))
            return false;
        if (this.createdOn == null) {
            if (other.createdOn != null)
                return false;
        }
        else if (!this.createdOn.equals(other.createdOn))
            return false;
        return true;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
        result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
        result = prime * result + ((this.status == null) ? 0 : this.status.hashCode());
        result = prime * result + ((this.createdOn == null) ? 0 : this.createdOn.hashCode());
        return result;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("User (");

        sb.append(id);
        sb.append(", ").append(name);
        sb.append(", ").append(status);
        sb.append(", ").append(createdOn);

        sb.append(")");
        return sb.toString();
    }
}

Explanation

Basically, the result of the query invocation includes all the fields from the user table, plus an additional field representing the rank. The problem arises because, when dealing with a standard Java class, the object is created using a no-argument constructor, and then setters are used to populate the fields. The additional, unknown field rank is simply ignored.

But in the case of java record, there are no setters. And since there is an additional field in the result of the query invocation, the all-arguments constructor becomes unsuitable. This is why the mapping cannot be performed automatically and the exception is thrown. To resolve this, you either need to implement the mapping manually or remove the additional field from the result.