Search code examples
javahibernatejpaeclipselinkjpql

JPA CriteriaBuilder to generate query where Map is empty using @MapKeyColumn


Following referenciation situation of entities:

Collision -> CollisionStatus <-> CollisionWorkgroup (join entity) -> Workgroup

Collision.java:

@Entity
@Table( name = "..." )
public class Collision
{
    ...

    @OneToOne( fetch = FetchType.EAGER, optional = false )
    @JoinColumn( name = "CSSTATE_ID", referencedColumnName = "CSSTATE_ID" )
    private CollisionStatus collisionStatus;

    ...
}

CollisionStatus.java: (class of interest, see below)

@Entity
@Table( name = "..." )
public class CollisionStatus
{
    ...

    // THIS IS THE MAPPING OF INTEREST:
    @OneToMany( mappedBy = "collisionStatus", fetch = FetchType.LAZY )
    @MapKeyColumn( name = "CLIENT_ID", insertable = false, updatable = false )
    private Map<Long, CollisionWorkgroup> collisionWorkgroups;

    ...
}

CollisionWorkgroup.java: (join entity/table between CollisionStatus and Workgroup, with PK=[CollisionStatusId, ClientId], both type Long )

@Entity
@Table( name = "..." )
public class CollisionWorkgroup
{
    @EmbeddedId
    protected CollisionWorkgroupEmbeddedPK pk;

    @MapsId( "collisionStatusId" )
    @JoinColumn( name = "CSSTATE_ID", referencedColumnName = "CSSTATE_ID" )
    private CollisionStatus     collisionStatus;

    @MapsId( "clientId" )
    @ManyToOne
    @JoinColumn( name = "CLIENT_ID", referencedColumnName = "CLIENT_ID" )
    private Client              client;

    @ManyToOne( fetch = FetchType.LAZY, optional = false )
    @JoinColumn( name = "WORKGROUP_ID", referencedColumnName = "WORKGROUP_ID" )
    private Workgroup           workgroup;

    ...
}

CollisionWorkgroupEmbeddedPK.java: (join entity PK class)

@Embeddable
public class CollisionWorkgroupEmbeddedPK implements Serializable
{
    private static final long serialVersionUID = 1L;

    @Column( name = "CSSTATE_ID" )
    private Long              collisionStatusId;

    @Column( name = "CLIENT_ID" )
    private Long              clientId;

    ...
}

Workgroup.java: (actually not very interesting, just used to compare the clientId in the criteria query)

@Entity
@Table( name = "..." )
public class Workgroup
{
    @Column( name = "CLIENT_ID", insertable = false, updatable = false )
    private Long              clientId;

    @ManyToOne( fetch = FetchType.EAGER, optional = false )
    @JoinColumn( name = "CLIENT_ID", referencedColumnName = "CLIENT_ID" )
    private Client            client;

    ...
}

The interesting mapping in this is from CollisionStatus to the join entity CollisionWorkgroup to connect the workgroups using the @MapKeyColumn.

What this means is, that every client can associate exactly one workgroup with a collision's status entity. Every user logged in to the system only sees the one from its client (every user belongs to one client), but the other ones are not to be visible on the UI.

When executing the query as-is, any collision statuses to which multiple clients have set a workgroup produces additional results in our data table/query results.

This makes sense, so I need to add some custom predicates to the query to only generate entries that have the current user's client ID associated. Showing entries of other clients is wrong.

I found this on SO:

Using JPA CriteriaBuilder to generate query where attribute is either in a list or is empty

I tried the code:

@Override
protected List<Predicate> createCustomPredicates( CriteriaBuilder builder, From<?, ?> root )
{
    List<Predicate> predicates = new ArrayList<>();

    Long clientId = this.sessionHelper.getCurrentClientId();
    Join<Collision, CollisionStatus> collisionStatus = root.join( "collisionStatus" );
    MapJoin<CollisionStatus, Long, CollisionWorkgroup> collisionWorkgroups = collisionStatus.<CollisionStatus, Long, CollisionWorkgroup>joinMap( "collisionWorkgroups", JoinType.LEFT );
    predicates.add( builder.and( builder.or( builder.isEmpty( collisionWorkgroups ),
                                             builder.equal( collisionWorkgroups.<String>get( "workgroup" ).<String>get( "clientId" ), clientId ) ) ) );

    ...
}

This gives me a compile error on builder.isEmpty( saying:

Bound mismatch: The generic method isEmpty(Expression<C>) of type CriteriaBuilder
is not applicable for the arguments (MapJoin<CollisionStatus,Long,CollisionWorkgroup>).
The inferred type CollisionWorkgroup is not a valid substitute for the bounded
parameter <C extends Collection<?>>

Obviously, the problem is that a Map is not a subclass of Collection.

Q:

How do you test for an empty Map in JPA using the Criteria API?


Solution

  • Right, because a Map is not a Collection. JPA only defines the IS_EMPTY predicate in terms of Collection as far as the Criteria API is concerned. Because of that and the associated CriteriaBuilder method signature you will not be able to do that through JPA's CriteriaBuilder.

    Really JPA should add an overload for that method:

    // existing method
    <C extends Collection<?>> Predicate isEmpty(Expression<C> collection);
    // overload
    <C extends Map<?>> Predicate isEmpty(Expression<C> map);
    

    JPQL however should work.