'org.jooq.exception.MappingException: No DefaultRecordMapper strategy applies...' when using Java Records as POJOs
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.