Search code examples
jpahibernate-criteria

JPA Criteria API filtering on subclasses' attribute


I have the following entity domain.

Parent class

@Entity
public final class SupportModuleInteraction implements Serializable {

    @Id
    private Long id;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "supportModuleInteraction")
    @OrderColumn
    private List<Event> events;

    // ... getters/setters/ other attributes ommitted
}

Child class hierachy

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "event_type")
public abstract class Event implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(fetch = FetchType.EAGER)
    private SupportModuleInteraction supportModuleInteraction;
}


@Entity
@DiscriminatorValue("SEARCH")
public final class SearchEvent extends Event {

    @Lob
    private String searchTerm;
}

I'm trying to retrieve any SupportModuleInteractions that contain a SearchEvent containing a keyword.

This is the Specification I'm using to try and retrieve those SupportModuleInteractions:

public static Specification<SupportModuleInteraction> interactionWithSearchEventContaingKeyword(String keyword) {
        return new Specification<SupportModuleInteraction>() {
            @Override
            public Predicate toPredicate(Root<SupportModuleInteraction> smi, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                Join<SupportModuleInteraction, SearchEvent> events = smi.join("events");
                return criteriaBuilder.and(
                        criteriaBuilder.equal(events.type(), SearchEvent.class),
                        criteriaBuilder.like(events.get("searchTerm"), "%" + keyword + "%"));
            }
        };
    }

The Specification fails saying that Event contains no attribute called "searchTerm" which makes sense as that's present only on the SearchEvent subclass.

I've looked at this answer here JPA2 Criteria queries on entity hierarchy which doesn't solve my issue as the entity involved in the query lies outside the class hierarchy.

As a native SQL query, this is what I'm trying to acheive:

SELECT *
FROM event E JOIN support_module_interaction smi ON E.support_module_interaction_id = smi.id
WHERE event_type = 'SEARCH'
  AND search_term LIKE 'searchString'

UPDATE 31/08/2019

I've looked at JPA Criteria API: query property of subclass and revised the Specification to use criteriaBuilder.treat(...) with no luck:

 public static Specification<SupportModuleInteraction> interactionWithSearchEventContaingKeyword(String keyword) {
        return new Specification<SupportModuleInteraction>() {
            @Override
            public Predicate toPredicate(Root<SupportModuleInteraction> smi, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                Join<SupportModuleInteraction, SearchEvent> events = smi.join("events");
                return criteriaBuilder.and(
                        criteriaBuilder.equal(events.type(), SearchEvent.class),
                        criteriaBuilder.like(
                                criteriaBuilder.treat(events.get("searchTerm"), SearchEvent.class).get("searchTerm"),
                                "%" + keyword + "%"));
            }
        };
    }

Solution

  • The various inheritance strategies in Hibernate are rather difficult to work with in the JPA Crtieria API.

    Instead of using treat(), which is rather finicky (see HHH-9862 and related issues), generate a subquery() expression which filters for searchTerm and add that to your parent query.

    Your overall implementation is unclear, so I created a basic example:

    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<SupportModuleInteraction> cq = cb.createQuery(SupportModuleInteraction.class);
    
    Root<SupportModuleInteraction> supModIntRoot = cq.from(SupportModuleInteraction.class);
    Join<SupportModuleInteraction, Event> supModIntEventJoin = supModIntRoot.join("events"); 
    
    // Instantiate the Subquery and Root objects for your subclass entity
    Subquery<SearchEvent> searchEventSubquery = cq.from(SearchEvent.class);
    Root<SearchEvent> searchEventRoot = searchEventSubquery.from(SearchEvent.class);
    // Generate the subquery and filter based on the given parameter
    searchEventSubquery.select(searchEventRoot);
    searchEventSubquery.where(cb.like(searchEventRoot.get("searchTerm", "%" + searchTerm + "%");
    
    // add subquery to where clause in parent query
    cq.select(supModIntRoot);
    cq.where(supModIntEventJoin.in(searchEventSubquery), //other subqueries and filters...);