I'm using the Blaze Persistence API with Spring Data JPA's specification API. When querying the database via EntityViewSpecificationExecutor with a Pageable object, duplicates are returned. How can I eliminate these duplicates?
In the below dummy scenario, I am trying to let my users filter course names by language, as well as other fields that I have removed from this question for brevity. A course can have multiple names in another language. For example, a course can have two Spanish names and three Greek names.
In my application, I have two entities:
@Data
@Entity
@Table(name = "courses")
public class Course implements Serializable {
@Id
@Column(name = "course_id")
private Long id;
@Column(name = "course_name")
private String courseName;
@OneToMany(mappedBy = "anCourse", cascade = CascadeType.ALL)
private List<AltName> altNameList;
// Other fields and relationships removed for brevity
}
@Data
@Entity
@Table(name = "alt_names")
public class AltName implements Serializable {
@Id
@Column(name = "alt_id")
private Long id;
@Column(name = "alt_name")
private String altName;
@Column(name = "alt_language")
String altNameLanguage; // A course can have multiple alt names in the same language
@ManyToOne
private Course anCourse;
// Several other fields removed for brevity
And I have entity views:
EntityView(Course.class)
public interface SearchResultView extends CourseView {
@Mapping(fetch = FetchStrategy.MULTISET)
public List<AltNameView> getAltNameList();
// Other multiset mappings removed for brevity
}
@EntityView(CourseView.class)
public interface CourseView {
@IdMapping
public Long getCourseId();
public String getCourseName();
}
@EntityView(AltName.class)
public interface AltNameView {
@IdMapping
public int getId();
public String getAltName();
public String getAltNameLanguage();
// Other fields removed for brevity
}
And this is how I'm attempting to query the database:
@Transactional(readOnly = true)
public interface CourseBlazeRepository extends EntityViewRepository<SearchResultView, Long>,
EntityViewSpecificationExecutor<SearchResultView, Course> {
Page<SearchResultView> findCourses(Specification<Course> courseSpecification, Pageable pageable);
}
// The below is expected to find zero, one, or more Spanish course names
public static Specification<Course> filterForSpanishLanguage(){
return ((root, criteriaQuery, criteriaBuilder) -> {
Join<Course, AltName> altNames = root.join("altNameList", JoinType.INNER);
return criteriaBuilder.equal(altNames.get(AltName_.ALT_NAME_LANGUAGE), "Spanish");
});
}
I'm using SpringBoot 3.3.3, Blaze persistence 1.6.12, and Hibernate 6.
As a workaround, I'm thinking I might have to use Blaze with the plain criteria API/follow demos from here, but I'd rather not have to do that if not necessary.
The scenario described above is a dummy scenario meant to reflect my actual application. Please disregard any oddities.
Note: If I use the "vanilla" JpaSpecificationExecutor from the Spring Data JPA library with the Pageable object, I am able to use criteriaQuery.distinct(true);
, and the query returns the expected number of records. If I try to adjust the criteria query like this while using the EntityViewSpecificationExecutor, Spring throws the runtime exception "Cannot paginate a DISTINCT query" because the Blaze-generated query contains a distinct keyword.
The problem is that your specification will alter the cardinality of the result list because you are joining a *-to-many association. You should rather make use of an exists subquery predicate to essentially semi-join the altNameList
like this:
// The below is expected to find zero, one, or more Spanish course names
public static Specification<Course> filterForSpanishLanguage(){
return ((root, criteriaQuery, criteriaBuilder) -> {
Subquery<Integer> subquery = criteriaQuery.subquery(Integer.class);
subquery.select(cb.literal(1));
Root<Course> correlatedRoot = subquery.correlate(root);
Join<Course, AltName> altNames = correlatedRoot.join("altNameList", JoinType.INNER);
subquery.where(criteriaBuilder.equal(altNames.get(AltName_.ALT_NAME_LANGUAGE), "Spanish"));
return criteriaBuilder.exists(subquery);
});
}