sqlhibernatejpablaze-persistence

JPA different sorting result with Criteria API and native SQL


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:

  1. WebResource
@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;
}
  1. Review
@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;
}
  1. User
@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;
}
  1. ReviewLike
@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!


Solution

  • 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