46

I have the following Specification that I use to query for any Contact entities that are tied to certain ManagedApplication entities. I pass in a Collection<Long> that contains the ids of the ManagedApplication entities that I am searching for.

public static Specification<Contact> findByApp(final Collection<Long> appIds) {
    return new Specification<Contact>() {
        @Override
        public Predicate toPredicate(Root<Contact> root, CriteriaQuery<?> query, CriteriaBuilder cb) {            
            final Predicate appPredicate = root.join(Contact_.managedApplications)
                .get(ManagedApplication_.managedApplicationId).in(appIds);
        }
    }
}

I pass this specification to the .findAll() method of my PagingAndSoringRepository to retrieve a Page<Contact> that will contain all Contact entities that meet the search criteria.

Here is the Repository.

@Repository
public interface PagingAndSortingContactRepository extends PagingAndSortingRepository<Contact, Long>, JpaSpecificationExecutor<Contact> {    
}

And here is how I'm calling the .findAll() method.

final Page<Contact> contacts = pagingAndSortingContactRepository.findAll(ContactSpecification.findByApp(appIds), pageable);

This works and returns all Contact entities that are tied to any of the ManagedApplication entities that correspond to the ids passed in. However, since I am calling .join() to join the Contact entity with the ManagedApplication entity, if one Contact has multiple ManagedApplication entities in the list of app ids, then the query will return duplicate Contact entities.

So what I need to know is, how can I get only distinct Contact entities returned from my query using this Specification?

I know that CriteriaQuery has a .distinct() method that you can pass a boolean value to, but I am not using the CriteriaQuery instance in the toPredicate() method of my Specification.

Here are the relevant sections of my metamodels.

Contact_.java:

@StaticMetamodel(Contact.class)
public class Contact_ {
    public static volatile SingularAttribute<Contact, String> firstNm;
    public static volatile SingularAttribute<Contact, String> lastNm;
    public static volatile SingularAttribute<Contact, String> emailAddress;
    public static volatile SetAttribute<Contact, ManagedApplication> managedApplications;
    public static volatile SetAttribute<Contact, ContactToStructure> contactToStructures;
}

ManagedApplication_.java

@StaticMetamodel(ManagedApplication.class)
public class ManagedApplication_ {
    public static volatile SingularAttribute<ManagedApplication, Integer> managedApplicationId;
}
Andrew Mairose
  • 10,615
  • 12
  • 60
  • 102

2 Answers2

92

Use the query parameter in your toPredicate method to invoke the distinct method.

Sample below:

public Predicate toPredicate(Root<Contact> root, CriteriaQuery<?> query, CriteriaBuilder cb) {            
    final Predicate appPredicate = root.join(Contact_.managedApplications)
        .get(ManagedApplication_.managedApplicationId).in(appIds);
    query.distinct(true);
    ...
Ish
  • 3,992
  • 1
  • 17
  • 23
  • 7
    Thank you for this solution, it works great! Also, to clarify, you have to add the `query.distinct(true)` to every predicate that needs this distinct statement. Its not sufficient to just add this statement to any predicate and have it work for the entire query.. – Dieter Hubau Nov 28 '16 at 08:21
  • How does this work? I mean, if you use plain MySQL syntax to join some tables that form a many to many relationship so that you can select the rows of the left table which have a certain value in a column of the right table, although you use a SELECT DISTINCT clause you still get duplicated results if a row in the left table is related to more than one row in the right table. – user3289695 Oct 02 '19 at 13:28
  • 1
    It seems that manipulating the `query` parameter in a `Specification` is [discouraged](https://github.com/spring-projects/spring-data-jpa/issues/2126#issuecomment-752569941) and [unsupported](https://github.com/spring-projects/spring-data-jpa/issues/2126#issuecomment-752569947) by the devs. But I don't have a better solution to offer. – Édouard Aug 09 '21 at 22:32
23

a new static method could be added

public static Specification<Object> distinct() {
    return (root, query, cb) -> {
        query.distinct(true);
        return null;
    };
}

where later you could add when creating your Specification

Specification.where(
    YourStaticClassWhereYouCreatedTheUpperMethod.distinct().and(..))
Panos Nikolos
  • 333
  • 3
  • 11