Spring JPA using @MappedSuperclass joins the wrong entity type in QueryUtils when requesting Pageable w/ Sort

Context:

In Kotlin, I have entity classes that are versioned, so there’s the general entity class “EntityA” and the entity “EntityAVersion”.

@Entity
@Table(name = "entity_a")
class EntityA(

//single instanceCount field

) : VersionedEntity<EntityA, EntityAVersion>()

as well the versioned entities

@Entity
@Table(name = "entity_a_version")
@JsonInclude(JsonInclude.Include.NON_NULL)
class EntityAVersion(
    
    //Many columns unique to this entity A

) : EntityVersion<EntityAVersion, EntityA>()

Imagine that there are pairs of these for B, C…G as well.

The VersionedEntity class, from which EntityA inherits, joins EntityA with EntityAVersion

@MappedSuperclass
abstract class VersionedEntity<Self : VersionedEntity<Self, V>, V : EntityVersion<V, Self>>(
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "current_version_id", referencedColumnName = "id")
    var latest: V? = null,

    //other stuff
) : BaseEntity()

The Problem:

The ORM works fine for all the basic CRUD stuff until trying to make a request using a Pageable that contains a Sort for one of fields in EntityA.latest.someField. As JPA, inside its QueryUtils class, walks down the path of EntityA to latest to someField, it establishes how the entities join together.

However, it incorrectly decides that the type of latest is EntityAVersion, regardless of what it should be (B, C, D, etc.).

From inside QueryUtils in JPA 2.6.10

private static boolean requiresOuterJoin(From<?, ?> from, PropertyPath property, boolean isForSelection,
            boolean hasRequiredOuterJoin) {

        String segment = property.getSegment(); //at this point, segment is "latest"

        ...

        Bindable<?> propertyPathModel;
        Bindable<?> model = from.getModel();

        ManagedType<?> managedType = null;
        if (model instanceof ManagedType) {
            managedType = (ManagedType<?>) model;
        } else if (model instanceof SingularAttribute
                && ((SingularAttribute<?, ?>) model).getType() instanceof ManagedType) {
            managedType = (ManagedType<?>) ((SingularAttribute<?, ?>) model).getType();
        }
        if (managedType != null) {

            //HERE IS THE PROBLEM: "getAttribute(segment)" only ever returns "EntityAVersion"
            propertyPathModel = (Bindable<?>) managedType.getAttribute(segment);
        } else {
            propertyPathModel = from.get(segment).getModel();
        }
...
}

in the last if block, propertyPathModel = (Bindable<?>) managedType.getAttribute(segment); is where it goes awry. Stepping into “getAttribute()” brings us into the AbstractManagedType class
from Hibernate-core:5.6.15.Final:

public PersistentAttributeDescriptor<? super J, ?> getAttribute(String name) {
        PersistentAttributeDescriptor<? super J, ?> attribute = (PersistentAttributeDescriptor)this.declaredAttributes.get(name);
        if (attribute == null && this.getSuperType() != null) {
            attribute = this.getSuperType().getAttribute(name);
        }

        this.checkNotNull("Attribute ", attribute, name);
        return attribute;
    }

The line this.getSuperType().getAttribute(name) (where name is “latest”) seemingly hopelessly only ever returns a SingularAttributeImpl of type EntityVersionA, irrespective of the “path” beginning with a different entity, e.g. “EntityB.latest.someFieldUniqueToB”.

The consequence of it being the wrong type of Bindable is that it only ever left joins to EntityVersionA, which causes JPA to bungle the sorting (the field unique to EntityVersionB, for instance, would never be found) and a stacktrace about not finding that property is printed.

Is there something that should change about the ORM that can fix this?

Leave a Comment