I use JPA with Hibernate for the data access in PostgreSQL.
I need to sort Reviews
list by count of ReviewLikes with positive=true
. But when I try to get Reviews list (include all relations) with sorting and pagination I see different results with Criteria API and native SQL query.
If I use native SQL - sorting works fine.
If I use Criteria API returns wrong result. Sorting is done, but not completely, some elements are out of order.
Also I tried to use Blaze persistence, but unsuccessfully.
I suspect that the error is somewhere in my criteria, but I have been trying for the 4th day to find at least some similar case and without success.
Project has 4 entities:
@Entity
@Table(name = "web_resource")
public class WebResource {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
@Column(columnDefinition = "CHAR(36)")
private String id;
private String url;
@Enumerated(EnumType.STRING)
@Column(name = "resource_type")
private ResourceType resourceType;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="parent_id")
private WebResource parent;
}
@Entity
@Table(name = "reviews")
public class Review {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
@Column(columnDefinition = "CHAR(36)")
private String id;
@Column(name = "user_id")
private String userId;
@Column(name = "resource_id")
private String resourceId;
@Column(name = "review_id")
private String reviewId;
private Byte rating;
private String text;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "resource_id", nullable = false, insertable = false, updatable = false)
private WebResource webResource;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false, insertable = false, updatable = false)
private User user;
@OneToMany(mappedBy = "review", fetch = FetchType.LAZY)
private Set<ReviewLike> likes;
}
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
@Column(columnDefinition = "CHAR(36)")
private String id;
@Column(name = "user_name")
private String userName;
private String email;
private String password;
@Column(name = "full_name")
private String fullName;
@Column(name = "avatar_file_name")
private String avatarFileName;
@Column(name = "email_confirmed")
private Boolean emailConfirmed;
}
@Entity
@Table(name = "reviews_likes")
public class ReviewLike {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
@Column(columnDefinition = "CHAR(36)")
private String id;
@Column(name = "user_id")
private String userId;
@Column(name = "review_id")
private String reviewId;
private Boolean positive;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id", nullable = false, insertable = false, updatable = false)
private Review review;
}
I tried native SQL, and it works fine (ordering right):
select review.*
from reviews review
join web_resource webResource on review.resource_id = webResource.id
left join reviews_likes reaction on review.id = reaction.review_id
join users u on review.user_id = u.id
group by review.id, webResource.url, webResource.resource_type
order by sum(case when reaction.positive = true then 1 else 0 end) DESC
limit 10 offset 0;
But JPA Criteria API (Spring specification) returns list with wrong order:
List<Predicate> predicates = new ArrayList<>();
SetJoin<Review, ReviewLike> reviewLikeJoin = reviewRoot.joinSet(Review_.LIKES);
Expression<?> sortExpression sortExpression = criteriaBuilder.sum(
criteriaBuilder.<Integer>selectCase()
.when(criteriaBuilder.equal(reviewLikeJoin.get(ReviewLike_.POSITIVE), true), 1)
.otherwise(0)
);
criteriaQuery.groupBy(reviewRoot.get(Review_.ID));
criteriaBuilder.desc(sortExpression);
Blaze persistence doesn't work correctly too:
CriteriaBuilder<Review> criteriaBuilder = criteriaBuilderFactory.create(entityManager, Review.class, "review")
.leftJoinFetch("review.likes", "reviewLike")
.leftJoinFetch("review.webResource", "webResource")
.leftJoinFetch("review.user", "user")
.groupBy("review.id", "review.likes.reviewId", "review.likes.id")
.orderByDesc("SUM(case when reviewLike.positive = true then 1 else 0 end)");
List<Review> reviews = criteriaBuilder.getResultList();
I will be grateful for any help!
The main problem was not in Criteria API, but in the bad logic of SQL query which I expected. I refactored expected SQL query and I was able to implement it in the Criteria API.
Example for right SQL logic:
select e, (select count(*) from r.likes l where l.positive = true) as likeCount
from Review r
join fetch r.thing
join fetch r.user
left join fetch r.likes
order by likeCount desc