3

I have the following entities

Book {
  private String title;
  
  @ManyToMany
  @JoinTable(
     name = "book_should_read_user",
     joinColumns = @JoinColumn(name = "book_id"),
     inverseJoinColumns = @JoinColumn(name = "user_id")
  )
  private Set<User> shouldRead; // the users who should read the book

  @OneToMany(mappedBy = "book")
  private Set<BookReadAction> actions; // all the read actions performed on the book
}
BookReadAction {
  @ManyToOne
  private Book book;

  @ManyToOne
  private User user;
}

Now I want to query all books that have been read by all the users in the should read collection. The following sql query in postgres does the trick :

select * 
from book 
where id in (
   select distinct id 
   from (
      select book.id id, 
             array_agg(book_should_read_user.user_id) suggested_readers, 
             array_agg(distinct book_read_action.user_id) read_by 
     from book b
         inner join book_should_read_user on book.id = book_should_read_user.book_id
         inner join book_read_action on book.id = book_read_action.book_id
     group by book.id) subquery
   where suggested_readers <@ read_by)

I however want to add this clause programmatically so i'd rather use JPA criteria API. Despite some attempts i struggled. Is it possible to build a Predicate from this query in JPA criteria API?

2 Answers 2

2

You can't exactly write the query you wrote as HQL but an equivalent one:

select * 
from book b
where exists (
     select 1
     from book b
         inner join book_should_read_user on book.id = book_should_read_user.book_id
         inner join book_read_action on book.id = book_read_action.book_id
     where b.id = book.id
     group by book.id
     having array_agg(book_should_read_user.user_id) <@ array_agg(distinct book_read_action.user_id)
)

To write this query with HQL or the JPA Criteria API you will need to provide a custom implemention of the <@ operator or the overall predicate through a SQLFunction that you can register in your Hibernate dialect. Something like this:

public class ArrayContainsFunction implements SQLFunction {
        
    @Override
    public boolean hasArguments() {
        return true;
    }
    
    @Override
    public boolean hasParenthesesIfNoArguments() {
        return true;
    }
    
    @Override
    public Type getReturnType(Type firstArgumentType, Mapping mapping) throws QueryException {
        SessionFactoryImplementor sfi = (SessionFactoryImplementor) mapping;
        return sfi.getTypeHelper().basic(Integer.class);
    }
    
    @Override
    public String render(Type firstArgumentType, List args, SessionFactoryImplementor factory) throws QueryException {
        return "array_agg(" + args.get(0) + ") <@ array_agg(" + args.get(1) + ") and 1=";
    }
}

When registering it, you should be able to use like this in HQL ... HAVING array_contains(shouldRead.id, readAction.id)

Sign up to request clarification or add additional context in comments.

1 Comment

Thx for the advice, it is indeed the correct approach for my problem
0

With criteria API you cannot implement a subquery as from clause.

https://www.objectdb.com/java/jpa/query/jpql/from#FROM_and_JOIN_in_Criteria_Queries

You would have to restructure your query

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.