Do I really need countQuery in a native #Query for pagination? - spring-data-jpa

According to the reference, countQuery is required for native queries pagination (at least there is an example with it). But according to my tests (Spring Boot 2.4.5), #Query pagination works without countQuery for such queries:
public interface ARepository extends JpaRepository<A, Long>{
#Query(value = "select * from a", nativeQuery = true)
Page<A> findAllANative(Pageable pageable);
#Query(value = "select count(a.id) as aCount, category.name as
categoryName " +
"from a " +
"join category on a.category_id=category.id " +
"group by category.name",
nativeQuery = true)
List<CountView> findACountByCategoryNative(Pageable pageable);
}
As you can see, there is no countQuery and they work.
Does pagination work only for these specific queries or I can omit countQuery in all cases?

Related

Spring Data query: how to express 'at least one of the element of this list is member of...'

For now, I have a Repository class, that has a Department and a list of Users.
For a connected user, I want to retrieve all repositories from a dedicated department or repositories where the user is listed.
Here is how it is managed using Spring Data query:
#Query(value = "select distinct repository " +
"from Repository repository " +
"left join repository.users " +
"where analysis.department =:department " +
"or :connectedUser member of repository.users ",
countQuery = "select count(distinct repository) from Repository repository")
Page<Analysis> findAllByUser(Pageable pageable,
#Param("department") Department department,
#Param("connectedUser") User connectedUser);
It is working well.
I have now to integrate a new feature.
The repository can be linked to a list of UserGroup.
I would like to update my method with something like this:
#Query(value = "select distinct repository " +
"from Repository repository " +
"left join repository.users " +
"left join repository.userGroups " +
"where analysis.department =:department " +
"or :connectedUser member of repository.users " +
"or (at least one element of :userGroupList) member of repository.userGroups ",
countQuery = "select count(distinct repository) from Repository repository")
Page<Analysis> findAllByUser(Pageable pageable,
#Param("department") Department department,
#Param("connectedUser") User connectedUser,
#Param("userGroupList") Set<UserGroup> userGroupList;
Is there a way to do that? If so, what should I use instead of (at least one element of userGroupList)?
Maybe, member of is not the best approach... If so, could you advise another one? My main objective is to be able to manage that in a single query to get page results.
Thanks in advance for all help and advice you could provide! :)
If it can help, here are entities used (high level, not in detail):
public class Repository {
private String name;
private Department department;
#ManyToMany
private Set<User> users = new HashSet<>();
#ManyToMany
private Set<UserGroup> userGroups = new HashSet<>();
public class UserGroup {
private String name;
private Department department;
private Set<User> users = new HashSet<>();

JPQL Create new object using a query with dynamic order by and limit

I am able to create my custom DTO by using this JPQL query:
#Query("SELECT new com.mycompany.dto.UserDetailsDTO(vu.id, ru.active, ru.firstname, ru.lastname, ru.username, vu.logins, ru.email, COUNT(li.creator)) "
+ "FROM User vu inner join RemoteUser ru on vu.remoteUser = ru.username "
+ "inner join Item li on li.creator = vu.id "
+ "group by li.creator, ru.active, ru.firstname, ru.lastname, ru.username, vu.logins, vu.id, ru.email")
List<UserDetailsDTO> getAllUsers();
Now I want to add order by, ASC/DESC, limit, offset to the above query to get the result based on dynamic params something like this:
#Query("SELECT new com.mycompany.dto.UserDetailsDTO(vu.id, ru.active, ru.firstname, ru.lastname, ru.username, vu.logins, ru.email, COUNT(li.creator)) "
+ "FROM User vu inner join RemoteUser ru on vu.remoteUser = ru.username "
+ "inner join Item li on li.creator=vu.id "
+ "group by li.creator, ru.active, ru.firstname, ru.lastname, ru.username, vu.logins, vu.id, ru.email "
+ "order by = :orderBy :orderDir and offset = :pageNo and limit = :pageSize")
List<UserDetailsDTO> getAllUsers(#Param("pageNo") int pageNo,
#Param("pageSize") int pageSize,
#Param("orderDir") String orderDir,
#Param("orderBy") String orderBy);
But it is not working and the error is:
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: = near line 1, column 427
Already tried with passing pageable as a param:
Page<UserDetailsDTO> getAllUsers(Pageable pageable)
and preparing page request like:
PageRequest pageRequest = PageRequest.of(pageNo, pageSize, Sort.by(Sort.Direction.valueOf(orderDir), orderFieldUid));
Also, I tried with
+ "order by ?4 ?3 and offset ?1 and limit ?2")
Is there any way to add dynamic params as order by and offset to it?
I want similar to something like this in JPQL.
Finally, I fixed it by using Page<UserDetailsDTO> getAllUsers(Pageable pageable) No need to pass params in the query. Hibernate will take the params internally from pageable

JPA - Find items from a list that don't exist in a table

Given a list of emails, I need to find which ones don't exist in a table. Using SQL, I can do the following:
SELECT e.email
FROM
(
VALUES('email1'),('email2'),('email3'),('email4')
) AS e(email)
EXCEPT
SELECT username FROM dbo.UsersTbl;
How can I write equivalent JPQL? In the application, values email1, email2... need be dynamically built (not hardcoded) based on passed in list. Using a Spring Data JPA native query, I can do the following:
#Query( value =
"SELECT e.email " +
" FROM " +
"( " +
" VALUES('email1'),('email2'),('email3'),('email4') " +
" ) AS e(email) " +
" EXCEPT " +
" SELECT username FROM dbo.UsersTbl ",
nativeQuery=true)
List<String> findMissingEmails(List<String> emails);
But how can I pass in the list of emails to the query?
For fixed number of email arguments, this could work:
#Query( value =
"SELECT e.email " +
" FROM " +
"( " +
" VALUES(:email1),(:email2),(:email3),(:email4) " +
" ) AS e(email) " +
" EXCEPT " +
" SELECT username FROM dbo.UsersTbl ",
nativeQuery=true)
List<String> findMissingEmails(String email1, String email2, String email3, String email4);
For high and/or dynamic number of emails, a better approach could be to use NativeQuery constructed at runtime.
Old answer - more or less exactly the opposite of what was asked for, but I'll keep it here as reference.
Using of named parameter:
#Query("SELECT u.email FROM User AS u WHERE u.email NOT IN (:emails)")
List<String> findMissingEmails(#Param("emails")Collection<String> emails);
Alternatively, you could use a JPA query method:
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findAllByEmailNotIn(Collection<String> emails);
}
Unfortunately that method would fetch and return a list of Users instead of list of their emails.
To fetch just emails you could use a JPA projection.
Assuming that User entity has a field of type String named email, the following projection could be used:
public interface UserEmail {
String getEmail();
}
And this is the repository method:
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<UserEmail> findAllByEmailNotIn(Collection<String> emails);
}

"Column count does not match" for h2 database Select query using projection in Spring-Boot #DataJpaTest

I have a Spring-Boot app that has some native queries that use content projection. It runs Postgres in production and works fine. I'm trying to set up integration tests for the repositories using #DataJpaTest and a h2 in-memory database, but my queries that are using content projection are failing with a JdbcSQLException out of the driver:
org.h2.jdbc.JdbcSQLException: Column count does not match
I successfully save to the TestEntityManager, so there are records in the database, but I am unable to invoke the SELECT via the repository method. It works properly in production on Postgres -- is this a limitation to h2 and is there a workaround I could apply so I can properly test this?
The repository method looks like this (one inner join, two params in the where clause, table names and columns changed to protect the guilty):
public interface OrderRepository extends PagingAndSortingRepository<Order, Long> {
#Query(nativeQuery = true,
value = "SELECT order.id, order.total, pizza.name " +
"FROM example.order " +
"INNER JOIN example.pizza USING (pizza_id) " +
"WHERE order.customer_id = :custId " +
"AND order.order_date = :orderDate ",
countQuery = "SELECT count(order.id) " +
"FROM example.order " +
"INNER JOIN example.pizza USING (pizza_id) " +
"WHERE order.customer_id = :custId " +
"AND order.order_date = :orderDate")
<T> Page<T> findAllByCustIdAndOrderDate(String custId, OffsetDateTime orderDate, Pageable paging, Class<T> type);
}
And the projection looks like this:
public interface PizzaOrderProjection {
Long getId();
Double getTotal();
String getName();
}
The exception triggers when I call findAllByCustIdAndOrderDate, and the SQL statement that it prints is causing it is the SELECT. The SELECT it prints looks perfectly normal:
Hibernate:
/* dynamic native SQL query */ SELECT
order.id,
order.total,
pizza.name
FROM
example.order
INNER JOIN
example.pizza USING (pizza_id)
WHERE
order.customer_id = ?
AND order.order_date = ? limit ?
2019-04-09 12:42:18.704 WARN 17568 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 21002, SQLState: 21S02
2019-04-09 12:42:18.708 ERROR 17568 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Column count does not match; SQL statement:
It turns out that the error message actually has nothing to do with the underlying issue.
The H2 database does not support the using keyword on the inner join clause, only the on keyword.
The issue was resolved by changing the inner join to use on instead, like this:
public interface OrderRepository extends PagingAndSortingRepository<Order, Long> {
#Query(nativeQuery = true,
value = "SELECT order.id, order.total, pizza.name " +
"FROM example.order " +
"INNER JOIN example.pizza ON order.pizza_id = pizza.pizza_id " +
"WHERE order.customer_id = :custId " +
"AND order.order_date = :orderDate ",
countQuery = "SELECT count(order.id) " +
"FROM example.order " +
"INNER JOIN example.pizza ON order.pizza_id = pizza.pizza_id " +
"WHERE order.customer_id = :custId " +
"AND order.order_date = :orderDate")
<T> Page<T> findAllByCustIdAndOrderDate(String custId, OffsetDateTime orderDate, Pageable paging, Class<T> type);
}
This change makes the queries valid in both postgres and h2.

Spring-Data-JPA native query on jsonb field

Given the following JSON which is stored as jsonb in a postgres table:
{
"object" : {
"array" [
{
"uuid" : "34ad3558-a3e7-43d0-826f-afddce255b20"
}
]
}
}
I have a working query to search if a field value is present within the JSON document:
select * from my_table
where my_json#>'{"object": {"array" : [{"field": "34ad3558-a3e7-43d0-
826f-afddce255b20"}]}}';
However when I try to replicate this query in Spring-Data-JPA using a native query on a JPARepository I keep getting the following exception:
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter with that position [1] did not exist
I initally tried:
#Query(value = "Select * From my_table"
+ "and my_json#>'{\"object\": {\"array\" : [{\"uuid\": \"?1\"}]}}'",
nativeQuery = true)
Set<MyEntity> myQuery(UUID uuid);
Following this I tried binding the parameter with #Param :
#Query(value = "Select * From my_table"
+ "and my_json#>'{\"object\"\\: {\"array\" \\: [{\"uuid\"\\: \":uuid\"}]}}'",
nativeQuery = true)
Set<MyEntity> myQuery(#Param("uuid") UUID uuid);
After that I tried converting the UUID to a String:
#Query(value = "Select * From my_table"
+ "and my_json#>'{\"object\"\\: {\"array\" \\: [{\"uuid\"\\: \":uuid\"}]}}'",
nativeQuery = true)
Set<MyEntity> myQuery(#Param("uuid") String uuid);
Still nothing works. The JPA entity looks like this :
#Entity
public class MyEntity {
#Id
private long id;
#Column("my_json")
private MyJson myJson
}
Other queries that invole the entity work fine with the jsonb field binding to MyJson entity. Is there a way to make this work?
You have to cast the value in your native query.
cast(:uuid as UUID)
So your query becomes.
#Query(value = "Select * From my_table"
+ "and my_json#>'{\"object\"\\: {\"array\" \\: [{\"uuid\"\\: cast(:uuid as UUID)}]}}'",
nativeQuery = true)