Spring / Jhipster QueryService how to build a deep specification

I’m new to QueryService generated with Jhispter, and I’m trying to use them to filter a list of an entity.

It works fine when I filter on fields of this entity, but now I have to do something a bit more complicated.

I have this data set :

  • Child <—– the entity I was talking about above
  • Parent <—– which has a list of Child
  • GrandParent <—– which has a list of Parent

What I have to do is :

  • Filter Childs by Parent.name
  • Filter Childs by GrandParent.name

So what I’ve done so far is :

private Specification<Child> createSpecification(ChildCriteria criteria) {
    Specification<Child> specification = Specification.where(null);
    if (criteria != null) {
        if (criteria.getParentName() != null) {
            speficiation = specification.and(buildReferringEntitySpecification(criteria.getParentName(), Child_.parent, Parent_.name));
        }
        if (criteria.getGrandParentName() != null) {
            specification.and(buildJoinSpecification(criteria.getGrandParentName(), Child_.parent, Parent_.grandParent, GrandParent_.name));
        }
    }
}

An extract of Child, Parent, GrandParent and ChildCriteria :

@Entity
@Table(name = "Child")
public class Child extends EntityObject {
    @ManyToOne(fetch = FetchType.LAZY)
    @JsonIgnoreProperties("childs")
    @QueryInit("GrandParent")
    private Parent parent;
}


@Entity
@Table(name = "Parent")
public class Parent extends EntityObject {
    @Column(name = "name")
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JsonIgnoreProperties("parents")
    private GrandParent grandParent;

    @OneToMany(mappedBy = "Parent", fetch = FetchType.LAZY)
    private Set<Child> childs = new HashSet<>();
}


@Entity
@Table(name = "GrandParent")
public class GrandParent extends EntityObject {
    @Column(name = "name")
    private String name;

    @OneToMany(mappedBy = "GrandParent", fetch = FetchType.LAZY)
    private Set<Parent> parrents = new HashSet<>();
}



public class ChildCriteria implements Serializable {
    private StringFilter parentName;
    private StringFilter grandParentName;

    public StringFilter getParentName() {
        return parentName;
    }

    public void setParentName(StringFilter parentName) {
        this.parentName= parentName;
    }

    public StringFilter getGrandParentName() {
        return grandParentName;
    }

    public void setGrandParentName(StringFilter grandParentName) {
        this.grandParentName= grandParentName;
    }
}

The filter on parent name works, and I’d like to do the same for the grand parent name. And for that one, I tried several tips I found on SO like this, but nothing worked.

From that link, here’s what I have so far :

public class ExtendedQueryService<ENTITY> extends QueryService<ENTITY>{
    protected <REFERENCE, JOIN, FILTER extends Comparable<? super FILTER>> Specification<ENTITY> buildJoinSpecification(StringFilter filter, SingularAttribute<? super ENTITY, REFERENCE> reference, SingularAttribute<REFERENCE, JOIN> joinField, SingularAttribute<JOIN, FILTER> valueField) {
        Specification<ENTITY> result = Specification.where((Specification) null);
        if (filter.getContains() != null) {
            result = this.containsSpecification(reference, joinField, valueField, filter.getContains());
        }
        return result;
    }

    protected  <REFERENCE, JOIN, FILTER> Specification<ENTITY> containsSpecification(SingularAttribute<? super ENTITY, REFERENCE> reference, SingularAttribute<REFERENCE, JOIN> joinField, SingularAttribute<JOIN, FILTER> idField, String value) {
        return (root, query, builder) ->
            builder.equal(root.join(reference).join(joinField).get(idField), value);
    }
}

I extend this class in my ChildQueryService, where I use the buildJoinSpecification method.

And it returns an empty list if I try to filter with it.

Answer

Alright, I found something in Jhipster doc. Problème was that I use a StringFilter, when I understood that, I modified few things from Jhipster functions like this :

ExtendedQueryService :

protected Specification<ENTITY> buildSpecification(StringFilter filter, Function<Root<ENTITY>, Expression<String>> metaclassFunction) {
        if (filter.getEquals() != null) {
            return equalsSpecification(metaclassFunction, filter.getEquals());
        } else if (filter.getIn() != null) {
            return valueIn(metaclassFunction, filter.getIn());
        } else if (filter.getNotIn() != null) {
            return valueNotIn(metaclassFunction, filter.getNotIn());
        } else if (filter.getContains() != null) {
            return likeUpperSpecification(metaclassFunction, filter.getContains());
        } else if (filter.getDoesNotContain() != null) {
            return doesNotContainSpecification(metaclassFunction, filter.getDoesNotContain());
        } else if (filter.getNotEquals() != null) {
            return notEqualsSpecification(metaclassFunction, filter.getNotEquals());
        } else if (filter.getSpecified() != null) {
            return byFieldSpecified(metaclassFunction, filter.getSpecified());
        }
        return null;
    }

So that I could do this in my ChildQueryService :

if (criteria.getGrandParentName() != null) {
    specification = specification.and(
        buildSpecification(
            criteria.getGrandParentName(),
            root -> root.join(Child_.parent, JoinType.INNER).join(Parent_.grandParent, JoinType.INNER).get(GrandParent_.name)
        )
    );
}

And it works fine now ! It seems you can go as deep as you want with the join, though I didn’t test it further than 2 level depth.

For the record, this is what I found in Jhipster doc, and what I based my function on :

protected <OTHER, MISC, X> Specification<ENTITY> buildReferringEntitySpecification(
    Filter<X> filter,
    Function<Root<ENTITY>, SetJoin<MISC, OTHER>> functionToEntity,
    Function<SetJoin<MISC, OTHER>, Expression<X>> entityToColumn
) {
    if (filter.getEquals() != null) {
        return equalsSpecification(functionToEntity.andThen(entityToColumn), filter.getEquals());
    } else if (filter.getSpecified() != null) {
        return byFieldSpecified(root -> functionToEntity.apply(root), filter.getSpecified());
    }
    return null;
}

which was used that way for instance :

buildReferringEntitySpecification(
    criteria.getGrandParentName(),
    root ->  root.get(Child_.parent).join(Parent_.grandParent),
    GrandParent_.name
)

Using both allows to handle StringFilter and other Filters. You might have to do specific function for some Filter class though.