Question

I am using Spring Data JpaRepository and I find it extremely easy to use. I actually need all those features - paging, sorting, filtering. Unfortunately there is one little nasty thing that seems to force me to fall back to use of plain JPA.

I need to order by a size of associated collection. For instance I have:

@Entity
public class A{
    @Id
    private long id;
    @OneToMany
    private List<B> bes;
//boilerplate
}

and I have to sort by bes.size()

Is there a way to somehow customize the ordering still taking the advantage of pagination, filtering and other Spring Data great features?

Was it helpful?

Solution

I've solved the puzzle using hints and inspirations from:

  1. Limiting resultset using @Query anotations by Koitoer
  2. How to order by count() in JPA by MicSim
  3. Exhaustive experiments on my own

The first and most important thing I've not been aware of about is that even using @Query custom methods one can still create paging queries by simply passing the Pageable object as parameter. This is something that could have been explicitely stated by spring-data documentation as it is definitely not obvious though very powerful feature.

Great, now the second problem - how do I actually sort the results by size of associated collection in JPA? I've managed to come to a following JPQL:

select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a

where AwithBCount is a class that the query results are actually mapped to:

public class AwithBCount{
    private Long bCount;
    private A a;

    public AwithBCount(Long bCount, A a){
        this.bCount = bCount;
        this.a = a;
    }
    //getters
}

Excited that I can now simply define my repository like the one below

public interface ARepository extends JpaRepository<A, Long> {
    @Query(
        value = "select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a",
        countQuery = "select count(a) from A a"
    )
    Page<AwithBCount> findAllWithBCount(Pageable pageable);
}

I hurried to try my solution out. Perfect - the page is returned but when I tried to sort by bCount I got disappointed. It turned out that since this is a ARepository (not AwithBCount repository) spring-data will try to look for a bCount property in A instead of AwithBCount. So finally I ended up with three custom methods:

public interface ARepository extends JpaRepository<A, Long> {
    @Query(
        value = "select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a",
        countQuery = "select count(a) from A a"
    )
    Page<AwithBCount> findAllWithBCount(Pageable pageable);

    @Query(
        value = "select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a order by bCount asc",
        countQuery = "select count(a) from A a"
    )
    Page<AwithBCount> findAllWithBCountOrderByCountAsc(Pageable pageable);

    @Query(
        value = "select new package.AwithBCount(count(b.id) as bCount,c) from A a join a.bes b group by a order by bCount desc",
        countQuery = "select count(a) from A a"
    )
    Page<AwithBCount> findAllWithBCountOrderByCountDesc(Pageable pageable);
}

...and some additional conditional logic on service level (which could be probably encapsulated with an abstract repository implementation). So, although not extremely elegant, that made the trick - this way (having more complex entities) I can sort by other properties, do the filtering and pagination.

OTHER TIPS

One option, which is much simpler than the original solution and which also has additional benefits, is to create a database view of aggregate data and link your Entity to this by means of a @SecondaryTable or @OneToOne.

For example:

create view a_summary_view as
select
   a_id as id, 
   count(*) as b_count, 
   sum(value) as b_total, 
   max(some_date) as last_b_date 
from b 

Using @SecondaryTable

@Entity
@Table
@SecondaryTable(name = "a_summary_view", 
       pkJoinColumns = {@PrimaryKeyJoinColumn(name = "id", referencedColumnName= "id")})
public class A{

   @Column(table = "a_summary_view")
   private Integer bCount;

   @Column(table = "a_summary_view")
   private BigDecimal bTotal;

   @Column(table = "a_summary_view")
   private Date lastBDate;
}

You can now then sort, filer, query etc purely with reference to entity A.

As an additional advantage you have within your domain model data that may be expensive to compute in-memory e.g. the total value of all orders for a customer without having to load all orders or revert to a separate query.

Thank you @Alan Hay, this solution worked fine for me. I just had to set the foreignKey attribute of the @SecondaryTable annotation and everything worked fine (otherwise Spring Boot tried to add a foreignkey constraint to the id, which raise an error for a sql View).

Result:

@SecondaryTable(name = "product_view",
            pkJoinColumns = {@PrimaryKeyJoinColumn(name = "id", referencedColumnName = "id")},
            foreignKey = @javax.persistence.ForeignKey(ConstraintMode.NO_CONSTRAINT))

I don't know much about Spring Data but for JPQL, to sort the objects by size of associated collection, you can use the query

Select a from A a order by a.bes.size desc

You can use the name of an attribute found in the select clause as a sort property:

@Query(value = "select a, count(b) as besCount from A a join a.bes b group by a", countQuery = "select count(a) from A a")
    Page<Tuple> findAllWithBesCount(Pageable pageable);

You can now sort on property besCount :

findAllWithBesCount(PageRequest.of(1, 10, Sort.Direction.ASC, "besCount"));

I used nativeQuery to arrange sorting by number of records from another table, pagable works.

 @Query(value = "SELECT * FROM posts where posts.is_active = 1 and posts.moderation_status = 'ACCEPTED' " +
                "group by posts.id order by (SELECT count(post_id) FROM post_comments where post_id = posts.id) desc",
                countQuery = "SELECT count(*) FROM posts",
                nativeQuery = true)
        Page <Post> findPostsWithPagination(Pageable pageable);

For SpringBoot v2.6.6, accepted answer isn't working if you need to use pageable with child's side field especially when using @ManyToOne.

For the accepted answer:

  • You can return new object with static query method, which have to include order by count(b.id)
  • And also order by bCount isn't working.

Please use @AlanHay solution, it is working, but you can't use primitive field and change foreign key constraint. For instance, change long with Long. Because:

When saving a new entity Hibernate does think a record has to be written to the secondary table with a value of zero. (if you use primitive type)

Otherwise you will get an exception: Caused by: org.postgresql.util.PSQLException: ERROR: cannot insert into view "....view"

Here is the example:

@Entity
@Table(name = "...")
@SecondaryTable(name = "a_summary_view,
        pkJoinColumns = {@PrimaryKeyJoinColumn(name = "id",
                referencedColumnName= "id")},
foreignKey =  @javax.persistence.ForeignKey(name = "none"))
public class UserEntity  {
    @Id
    private String id;
    @NotEmpty
    private String password;
    @Column(table = "a_summary_view",
            name = "b_count")
    private Integer bCount;

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top