Spring Data JPA #Query with spel in a IN query - spring

I work on Spring Data JPA and I have a repository interface which implements JpaRepository.
I have already written this query which works perfectly:
#Query ("FROM Person p " +
"LEFT JOIN p.relatedContractRoleAttributions rcras " +
"WHERE rcras.contract.id = :#{#contract.id} " +
"AND rcras.relatedContractRole.code = :#{#code}")
Person findByContractAndRelatedContractRole(#Param ("contract") Contract contract, #Param ("code") String code);
Now I want to write another query which can find in more than one code so I wrote this query:
#Query ("FROM Person p " +
"LEFT JOIN p.relatedContractRoleAttributions rcras " +
"WHERE rcras.contract.id = :#{#contract.id} " +
"AND rcras.relatedContractRole.code IN (:#{#codes})")
List<Person> findByContractAndRelatedContractRoles(#Param ("contract") Contract contract, #Param ("codes") String... codes);
But when I start my application I have this error:
Caused by: org.hibernate.QueryException: unexpected char: '#' [FROM com.krgcorporate.core.domain.access.Person p LEFT JOIN p.relatedContractRoleAttributions rcras WHERE rcras.contract.id = :#{#contract.id} AND rcras.relatedContractRole.code IN (:__$synthetic$__2)]
Do you have any idea why?
Thanks for your help.

I believe that there is an issue in org.springframework.data.jpa.repository.query.StringQuery. spring-data-jpa-1.7.2.RELEASE (lines 250..259)
case IN:
if (parameterIndex != null) {
checkAndRegister(new InParameterBinding(parameterIndex, expression), bindings);
} else {
checkAndRegister(new InParameterBinding(parameterName, expression), bindings);
}
result = query;
break;
So when StringQuery binds 'in' parameter with SPEL it overrides the result query string with the string in your #Query annotation. And then it replaces 'in' SPEL with the new binding.
If you change
"WHERE rcras.contract.id = :#{#contract.id} " +
"AND rcras.relatedContractRole.code in :#{#code}"
into
"WHERE rcras.relatedContractRole.code in :#{#code}" +
"AND rcras.contract.id = :#{#contract.id} "
it will fix your problem
UPDATE:
SpingDataJpa team has fixed it. https://jira.spring.io/browse/DATAJPA-712

For a JPA in clause you must not write brackets. (same question same answer here https://stackoverflow.com/a/4379008/280244)
#Query ("FROM Person p " +
"LEFT JOIN p.relatedContractRoleAttributions rcras " +
"WHERE rcras.contract.id = :#{#contract.id} " +
"AND rcras.relatedContractRole.code IN :#{#codes)")
And to be honest: I never have seen this syntax :#{#PARAM} before, I only know :PARAM. - but when it work in the other query is will work in the next one too
#Franck Yapadesouci Anso: according to your comment: are you sure that you can use the SPEL expressions in this way - try it the JPA way.
#Query ("SELECT p FROM Person p " + //+SELECT p
"LEFT JOIN p.relatedContractRoleAttributions rcras " +
"WHERE rcras.contract = :contract" + //without ID and SPEL
"AND rcras.relatedContractRole.code IN :codes") //without brackets and SPEL

Related

Fetch specific column by using multiple tables, using native query and interface based projection

my need is to use native query and interface-based projection in JPA to fetch the record from multiple tables
#Query(value = "select cm.center_code as centerCode,cm.center_name as centerName,es.exam_slot_code as slotName,ecm.total_seat_allocated as seatAllocatedCount,ecityM.city_name as centerCity,afcatg.name as groupName from center_master cm "
+ "inner join centre_examslot_mapping cesm "
+ "on cm.center_id = cesm.centre_id "
+ "inner join exam_city_master ecityM "
+ "on ecityM.city_id= cm.center_city_id "
+ "inner join exam_slot es "
+ "on cesm.exam_slot_id= es.exam_slot_id "
+ "inner join exam_center_mapping ecm "
+ "on cesm.centre_examslot_mappingid = ecm.centre_examslot_mappingid "
+ "inner join afcat_group afcatg "
+ "on ecm.afcat_group_id= afcatg.afcat_group_id "
+ "where cm.center_code= ?1", nativeQuery = true)
public List<GetSlotGroupSeatAllocatedFromDB> getData(String centerCode);
To use projections you have to define an interface with attributes you need with prefix 'get'. Your class GetSlotGroupSeatAllocatedFromDB must be an interface implemented like this:
public Interface GetSlotGroupSeatAllocatedFromDB{
Integer getCenterCode();
String getCenterName();
String getSlotName();
Integer getSeatAllocatedCount();
String getCenterCity();
String getGroupName();
}
I supposed the attributes types (Integer o String, maybe your centerCode is String...)

Spring Boot complex Query

I am trying to create a method in the repository to be able to find out what is the most recent contract signed by an employee. The problem I have is that if I want to filter by the start date of the contract, I also have to pass it as a parameter and I don't have that, that's what I need. I tried this but the result is all contracts of this employee.
#Query(value = "SELECT * "
+ "FROM Contracts c "
+ "WHERE c.employeeName = :name "
+ "AND c.dateStartContract = ("
+ "SELECT MAX(dateStartContract) "
+ "FROM Contracts d "
+ "WHERE d.employeeName = :name)")
public Contract findByContractIDEmployeeName(#Param("name") String name);
What you can do is to do a query to find all and sort it using descending order:
List<Contract> findAllByOrderByIdDesc();
Then you can just pick the first item of the list and that will represent the most recent contract signed.
You can also append so you can find by a specific name only:
List<Contract> findAllByOrderByIdDescAndName(String name);
It might be some slight error in my code since I have not had the opportunity to test it.
The problem was that the import of #Query at the repository was import org.springframework.data.jdbc.repository.query.Query; and must be import org.springframework.data.jpa.repository.Query;.
After this change, the consult to data base is exactly my Query request.
My query is this.
#Query(value = "SELECT c "
+ "FROM Contract c "
+ "WHERE c.contractID.employeeName = :name "
+ "AND c.contractID.contractStartDate = ("
+ "SELECT MAX(contractID.contractStartDate) "
+ "FROM Contract d "
+ "WHERE d.contractID.employeename = :name)")
public Contract findByContractIDNameEmployeeAndByContractIDDateStartContract(#Param("name") String name);

Is there a way to make these kind of query with multiple queries?

#Query("select user.id, language, profile " +
"from User user " +
"join user.language language " +
"left join user.profiles profile " +
"where user.id in :usersIds")
List<Object[]> findDataByUsersIds(#Param("usersIds") Collection<Integer> usersIds);
I would like a better way to write this query, by dividing it into multiple queries.

Spring Data JPA Pagination HHH000104

I got this repository code:
#Query(value = "select distinct r from Reference r " +
"inner join fetch r.persons " +
"left outer join fetch r.categories " +
"left outer join fetch r.keywords " +
"left outer join fetch r.parentReferences",
countQuery = "select count(distinct r.id) from Reference r " +
"inner join r.persons " +
"left outer join r.categories " +
"left outer join r.keywords " +
"left outer join r.parentReferences")
Page<Reference> findsAllRelevantEntries(Pageable pageable);
When i run tests on that query i got this Hibernate warning:
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
#Test
void testFindAllRelevantAsync() throws ExecutionException, InterruptedException {
CompletableFuture<Page<Reference>> all = referenceService.findAllRelevantAsync(PageRequest.of(1, 20));
CompletableFuture.allOf(all).join();
assertThat(all.get()).isNotNull();
assertThat(all.get()).isNotEmpty();
}
The repository code is encapsulated in a service method not shown here. It (the service method) just marshalls the call from the service to the repository and back.
Furthermore the generated sql query does not generate a limit clause. Though it does fire two queries.
One for the count, the other for the fetching of all records.
So it fetches all records and applies the pagination in memory.
This leads to a very slow query execution.
How can i make pagination work with this query?
EDIT
I know that this here is often suggested as solution:
How can I avoid the Warning "firstResult/maxResults specified with collection fetch; applying in memory!" when using Hibernate?
Is there a way to achieve the pagination with Spring Data JPA?
I donĀ“t want to hardwire either an EntityManager, neither i want to
extend code from a BasicTransformerAdapter
You could use a generic / reusable approach based on the two-queries approach.
One SQL query to retrieve the entities' IDs and a second query with an IN predicate including the IDs from the second query.
Implementing a custom Spring Data JPA Executor:
#NoRepositoryBean
public interface AsimioJpaSpecificationExecutor<E, ID extends Serializable> extends JpaSpecificationExecutor<E> {
Page<ID> findEntityIds(Pageable pageable);
}
public class AsimioSimpleJpaRepository<E, ID extends Serializable> extends SimpleJpaRepository<E, ID>
implements AsimioJpaSpecificationExecutor<E, ID> {
private final EntityManager entityManager;
private final JpaEntityInformation<E, ID> entityInformation;
public AsimioSimpleJpaRepository(JpaEntityInformation<E, ID> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
this.entityInformation = entityInformation;
}
#Override
public Page<ID> findEntityIds(Pageable pageable) {
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
CriteriaQuery<ID> criteriaQuery = criteriaBuilder.createQuery(this.entityInformation.getIdType());
Root<E> root = criteriaQuery.from(this.getDomainClass());
// Get the entities ID only
criteriaQuery.select((Path<ID>) root.get(this.entityInformation.getIdAttribute()));
// Update Sorting
Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
if (sort.isSorted()) {
criteriaQuery.orderBy(toOrders(sort, root, criteriaBuilder));
}
TypedQuery<ID> typedQuery = this.entityManager.createQuery(criteriaQuery);
// Update Pagination attributes
if (pageable.isPaged()) {
typedQuery.setFirstResult((int) pageable.getOffset());
typedQuery.setMaxResults(pageable.getPageSize());
}
return PageableExecutionUtils.getPage(typedQuery.getResultList(), pageable,
() -> executeCountQuery(this.getCountQuery(null, this.getDomainClass())));
}
protected static long executeCountQuery(TypedQuery<Long> query) {
Assert.notNull(query, "TypedQuery must not be null!");
List<Long> totals = query.getResultList();
long total = 0L;
for (Long element : totals) {
total += element == null ? 0 : element;
}
return total;
}
}
You can read more at https://tech.asimio.net/2021/05/19/Fixing-Hibernate-HHH000104-firstResult-maxResults-warning-using-Spring-Data-JPA.html
I found a workaround myself. Based upon this:
How can I avoid the Warning "firstResult/maxResults specified with collection fetch; applying in memory!" when using Hibernate?
First: Get the Ids by pagination:
#Query(value = "select distinct r.id from Reference r " +
"inner join r.persons " +
"left outer join r.categories " +
"left outer join r.keywords " +
"left outer join r.parentReferences " +
"order by r.id",
countQuery = "select count(distinct r.id) from Reference r " +
"inner join r.persons " +
"left outer join r.categories " +
"left outer join r.keywords " +
"left outer join r.parentReferences " +
"order by r.id")
Page<UUID> findsAllRelevantEntriesIds(Pageable pageable);
Second: Use the Ids to do an in query
#Query(value = "select distinct r from Reference r " +
"inner join fetch r.persons " +
"left outer join fetch r.categories " +
"left outer join fetch r.keywords " +
"left outer join fetch r.parentReferences " +
"where r.id in ?1 " +
"order by r.id",
countQuery = "select count(distinct r.id) from Reference r " +
"inner join r.persons " +
"left outer join r.categories " +
"left outer join r.keywords " +
"left outer join r.parentReferences ")
#QueryHints(value = {#QueryHint(name = "hibernate.query.passDistinctThrough", value = "false")},
forCounting = false)
List<Reference> findsAllRelevantEntriesByIds(UUID[] ids);
Note:
I get a List<Reference not a Pageable so you have to build your Pageable on your own like so:
private Page<Reference> processResults(Pageable pageable, Page<UUID> result) {
List<Reference> references = referenceRepository.findsAllRelevantEntriesByIds(result.toList().toArray(new UUID[0]));
return new PageImpl<>(references, pageable, references.size());
}
This looks not nice and does two statements, but it queries with limit, so only the needed records get fetched.
The approach to fetch ids first and then do the main query works but is not very efficient. I think this is a perfect use case for Blaze-Persistence.
Blaze-Persistence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. The pagination support it comes with handles all of the issues you might encounter.
It also has a Spring Data integration, so you can use the same code like you do now, you only have to add the dependency and do the setup: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-setup
Blaze-Persistence has many different strategies for pagination which you can configure. The default strategy is to inline the query for ids into the main query. Something like this:
select r
from Reference r
inner join r.persons
left join fetch r.categories
left join fetch r.keywords
left join fetch r.parentReferences
where r.id IN (
select r2.id
from Reference r2
inner join r2.persons
order by ...
limit ...
)
order by ...

Bind parameters for ORDER BY in NamedParameterJDBCTemplate

I am trying to use NamedParameterJdbTemplate in a Spring MVC application. The issue is that the bind parameters do not seem to work (no sorting happens) when I include one of ORDER BY clauses listed below. However, a hard coded order by column name in the sql works.
ORDER BY column1
ORDER BY column1
ORDER BY column1 asc
ORDER BY column1 desc
For example, the below listed query does not work.
private static final String SEARCH_ALL_BY_SORT_ORDER=
" select FIRST_NM, MIDDLE_NM, LAST_NM, CUSTOMER_IDENTIFIER, EMAIL_ADDRESS, ACCOUNT_ID" +
" from VIEW " +
" where CUSTOMER_IDENTIFIER= :customerIdentifier " +
" and ( REGEXP_LIKE(FIRST_NM, :firstName, 'i') " +
" or REGEXP_LIKE(LAST_NM, :lastName, 'i') " +
" or REGEXP_LIKE(EMAIL_ADDRESS, :emailAddress, 'i') )" +
" order by :sortColumns";
The same query with a hard coded order by column works:
private static final String SEARCH_ALL_BY_SORT_ORDER=
" select FIRST_NM, MIDDLE_NM, LAST_NM, CUSTOMER_IDENTIFIER, EMAIL_ADDRESS, ACCOUNT_ID" +
" from VIEW " +
" where CUSTOMER_IDENTIFIER= :customerIdentifier " +
" and ( REGEXP_LIKE(FIRST_NM, :firstName, 'i') " +
" or REGEXP_LIKE(LAST_NM, :lastName, 'i') " +
" or REGEXP_LIKE(EMAIL_ADDRESS, :emailAddress, 'i') )" +
" order by LAST_NM";
Here's the relevant jdbctemplate code
Map <String, Object> params = new HashMap <String, Object>();
params.put("customerIdentifier", customerIdentifier);
params.put("firstName", searchCriteria );
params.put("lastName", searchCriteria );
params.put("emailAddress",searchCriteria);
// sortBy is COLUMN name
// sortOrder is either 'asc' or 'desc'
params.put("sortColumns", sortBy + " " + sortOrder);
// Using just the column name does not work either
//params.put("sortColumns", sortBy);
namedParameterJdbcTemplate.query(SEARCH_ALL_BY_SORT_ORDER, params, new MemberMapper());
Only values can be bound as parameters. Not parts of the query itself.
In the end, a prepared statement is generated and the parameters are bound to the prepared statement. The principle of a prepared statement is to prepare the execution plan of the query (of which the order by clause is a part), and to execute the query one or several times after, with varying parameters.
If the query is not complete, the execution plan can't be prepared and reused. So, for this part of the query, you'll need to generate the query dynamically using string concatenation, and not parameters.
As JB Nizet has already explained that parts of query cannot be used as bind keys (orderby :age). Therefore we will need to use concatenation here instead.
" order by "+ sortBy + " " + sortOrder;

Resources