ClassCastException in JPA query - spring

I am trying to migrate from spring-boot 2.6.6 to spring-boot 3.0.2. Although, i get some really weird error in the custom queries which used to work in the previous version.
For example, in the following query:
entityManager.createQuery(
"SELECT distinct me FROM myEntity me " +
"WHERE me.deleted = false " +
"AND me.parent is null AND me.id IN (:ids) " +
"AND ((:type) is null OR me.type = :type) " +
"AND ((lower(me.label) LIKE lower(:name)) OR (lower(me.description) LIKE lower(:name))) " +
"AND ((:created) is null OR me.createdBy IN (:created)) " +
"AND ((:models) is null OR me.model IN (:models))")
.setParameter("name", "%" + name + "%").setParameter("ids", ids)
.setParameter("created", creators).setParameter("type", type)
.setParameter("models", models)
.getResultList()
This query is now throwing ClassCastException#786 "java.lang.ClassCastException: class java.util.ArrayList cannot be cast to class java.lang.String (java.util.ArrayList and java.lang.String are in module java.base of loader 'bootstrap')"
The variables are:
name: String
ids: List of Long
created: List of String
type: List of Enum
models: List of String
In the example i am trying to run name = "", ids contains 1 number which exists in the database, creators and type are null and models contains 1 string.
This issue is getting more weird as if i just run the following code then it works as expected:
entityManager.createQuery(
"SELECT distinct me FROM myEntity me " +
"WHERE me.deleted = false " +
"AND me.parent is null AND me.id IN (:ids) " +
"AND ((:type) is null OR me.type = :type) " +
"AND lower(me.description) LIKE lower(:name) " +
"AND ((:created) is null OR me.createdBy IN (:created)) " +
"AND ((:models) is null OR me.model IN (:models))")
.setParameter("name", "%" + name + "%").setParameter("ids", ids)
.setParameter("created", creators).setParameter("type", type)
.setParameter("models", models)
.getResultList()
Additionally, if i put two elements in models list then in the second case i get an index out of bounds error. Index: 0
Do you have any idea or proposal? I face that issue in multiple cases for String and Enum type lists.
Thank you a lot for your time!

Related

Spring boot 2 #Query named parameter binding value resolution messes up after upgrade from 1.5

We have the following working query using SpringBoot 1.5:
#Query(value = "SELECT DISTINCT c FROM Customer c INNER JOIN c.industry i WHERE " +
"c.role IN :roleFilter " +
"AND (:#{#industryFilter.size()} = 1 OR i.id IN :industryFilter) " +
"AND (:searchString IS NULL " +
"OR CONCAT_WS(' ', c.name, c.name2) LIKE CONCAT('%', :searchString, '%') " +
"OR CONCAT_WS(' ', c.name2, c.name) LIKE CONCAT('%', :searchString, '%')) " +
"AND (:includeDeleted = true OR c.deletedDate is NULL)",
countQuery = "SELECT COUNT(DISTINCT c) FROM Customer c INNER JOIN c.industry i WHERE " +
"c.role IN :roleFilter AND " +
"(:#{#industryFilter.size()} = 1 OR i.id IN :industryFilter) " +
"AND (:searchString IS NULL " +
"OR CONCAT_WS(' ', c.name, c.name2) LIKE CONCAT('%', :searchString, '%') " +
"OR CONCAT_WS(' ', c.name2, c.name) LIKE CONCAT('%', :searchString, '%')) " +
"AND (:includeDeleted = true OR c.deletedDate is NULL)")
Page<Customer> findCustomers(#Param("roleFilter") Set<Role> roleFilter,
#Param("industryFilter") Set<String> industryFilter,
#Param("searchString") String searchString,
#Param("includeDeleted") boolean includeDeleted, Pageable pageable);
Please note how we pass the input to the LIKE: CONCAT('%', :searchString, '%')
After upgrading from springBootVersion = '1.5.17.RELEASE' to springBootVersion = '2.1.3.RELEASE' (we use Gradle) that query will fail at runtime with an exception:
org.hibernate.QueryException: Named parameter not bound : includeDeleted
Replacing CONCAT('%', :searchString, '%') with %:searchString% fixes the problem.
The question I have is: why?
By going into debug mode and following the full callstack, I could see the parameters being correctly retrieved from the method invocation as observed in JdkDynamicAopProxy at line 205 makes a call Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); that results in:
argsToUse = {Object[5]#15562}
0 = {HashSet#15491} size = 4
1 = {HashSet#15628} size = 1
2 = null
3 = {Boolean#15629} false
4 = {PageRequest#15490} "Page request [number: 0, size 20, sort: name: ASC,name2: ASC]"
So far so good. Then, we keep going and the method to call is also correctly resolved:
parameterTypes = {Class[5]#15802}
0 = {Class#198} "interface java.util.Set"
1 = {Class#198} "interface java.util.Set"
2 = {Class#311} "class java.lang.String"
3 = {Class#15811} "boolean"
4 = {Class#9875} "interface org.springframework.data.domain.Pageable"
Then we go a bit further and we get to RepositoryFactorySupport line 599 calling private Object doInvoke(MethodInvocation invocation) throws Throwable which uses private final Map<Method, RepositoryQuery> queries; from the inner class public class QueryExecutorMethodInterceptor implements MethodInterceptor (I am unsure when/how was this variable created and populated), which contains all the queries annotated with #Query in my repository interface.
For our specific case, it contains an entry (last one) that matches the query I am invoking (findCustomers):
queries = {HashMap#16041} size = 3
0 = {HashMap$Node#16052} "public abstract com.swisscom.psp.domain.Customer com.swisscom.psp.repository.CustomerRepository.getOne(java.lang.String)" ->
1 = {HashMap$Node#16055} "public abstract boolean com.swisscom.psp.repository.CustomerRepository.existsWithRole(java.lang.String,java.util.Set)" ->
2 = {HashMap$Node#16058} "public abstract org.springframework.data.domain.Page com.swisscom.psp.repository.CustomerRepository.findCustomers(java.util.Set,java.util.Set,java.lang.String,boolean,org.springframework.data.domain.Pageable)" ->
And expanding that entry I can see where the error comes from, the binding for the :includeDeleted named parameter is simply not there:
value = {SimpleJpaQuery#16060}
query = {ExpressionBasedStringQuery#16069}
query = "SELECT DISTINCT c FROM Customer c INNER JOIN c.industry i WHERE c.role IN :roleFilter AND (:__$synthetic$__1 = 1 OR i.id IN :industryFilter) AND (:searchString IS NULL OR CONCAT_WS(' ', c.name, c.name2) LIKE CONCAT('%', :searchString, '%') OR CONCAT_WS(' ', c.name2, c.name) LIKE CONCAT('%', :searchString, '%')) AND (:includeDeleted = true OR c.deletedDate is NULL)"
bindings = {ArrayList#16089} size = 6
0 = {StringQuery$InParameterBinding#16092} "ParameterBinding [name: roleFilter, position: null, expression: null]"
1 = {StringQuery$ParameterBinding#16093} "ParameterBinding [name: __$synthetic$__1, position: null, expression: #industryFilter.size()]"
2 = {StringQuery$InParameterBinding#16094} "ParameterBinding [name: industryFilter, position: null, expression: null]"
3 = {StringQuery$ParameterBinding#16095} "ParameterBinding [name: searchString, position: null, expression: null]"
4 = {StringQuery$ParameterBinding#16096} "ParameterBinding [name: searchString, position: null, expression: null]"
5 = {StringQuery$ParameterBinding#16097} "ParameterBinding [name: searchString, position: null, expression: null]"
Now, I have the fix as mentioned earlier, but I would still very much like to know the following for future reference:
when and how is the private final Map<Method, RepositoryQuery> queries variable created and populated?
what exactly is causing this error? Did I miss something in the upgrade process? Am I using/mixing deprecated logic/wrong logic and should change the code further?
Our DB is MariaDB 10.1.36
EDIT: In all the places where this behaviour occurred (in some it still occurs), the unbound parameter is always the last one
EDIT2: Someone else also has a similar behaviour after the upgrade, why does this happen? reference
EDIT3: reference and also this weird behaviour has been reported. Interesting enough, I do not get the exception IF I pass already concatenated input to :searchString (eg: %SOMETHING%) and I do get the exception if I leave %:searchString% instead. And yes, moving those parameters in the end solves some errors I had with binding.
EDIT4: Maybe related bug?
Clearly there is something strange going on, so: how does this binding resolution happen exactly?
Thanks in advance and have a nice day
Actually, as far as I know, neither of your two approaches is the correct one to use here for handling LIKE with a wildcard placeholder. Instead, the LIKE expression should be:
LIKE :searchString
To this parameter :searchString you should be binding:
String searchString = "bananas";
String param = "%" + searchString + "%";
// then bind param to :searchString
That is, you bind the entire string, with the % wildcard, together. Then, let the database worry about how to escape it.

How to add a dynamic where in #Query?

I need to add my where conditions based in my filter object. If some value in the filter object is null, isn't necessary add the where condition of this parameter.
How to add a dynamic where?
Exemple:
FilterObject{
name: "Teste";
age: null;
}
#Query(value="SELECT id FROM user WHERE name = FilterObject.name", nativeQuery = true)
public List<User> getUsers(???)
Other example:
FilterObject{
name: "Teste";
age: 12;
}
#Query(value="SELECT id FROM user WHERE name = FilterObject.name AND age = FilterObject.age", nativeQuery = true)
public List<User> getUsers(???)
This is a highlevel example. Isnt the correct code. But... Is the necessary to understand my question
If a parameter of my filter is null this parameter isnt used in my where
Im Using spring boot with Spring Data JPA and my Query stay in my repository a Repository.
My Original request:
#Query(value = " SELECT "
+ " solicitacao.nm_orgao AS nmOrgao, "
+ " solicitacao.ds_ano_exercicio AS exercicio, "
+ " solicitacao.nm_organismo AS nmOrganismo, "
+ " dadosBancarios.valor AS valor, "
+ " dadosBancarios.valor * dadosBancarios.vl_taxa_cambio AS valorReais, "
+ " dadosFinanceiros.vl_dotacao_disponivel AS vlDotacaoDisponivel, "
+ " solicitacao.id_solicitacao AS codigoPagamento "
+ " solicitacao.tp_solicitacao_status AS idFase, "
+ " organismo.id_orgao AS idOrgao, "
+ " solicitacao.id_organismo AS idOrganismo, "
+ " pagamentos.id_status_pagamento AS statusPagamento "
+ " FROM "
+ " tbs_solicitacao solicitacao "
+ " JOIN tbs_dados_bancarios dadosBancarios ON solicitacao.id_solicitacao = dadosBancarios.id_solicitacao "
+ " JOIN tbs_dados_financeiros dadosFinanceiros ON dadosFinanceiros.id_solicitacao = solicitacao.id_solicitacao "
+ " JOIN tbs_organismo organismo ON organismo.id_organismo = solicitacao.id_organismo "
+ " JOIN tbs_pagamentos pagamentos ON dadosFinanceiros.id_dados_financeiros = pagamentos.id_dados_financeiros ",nativeQuery = true)
List<Object[]> getRelatoriosProgramacao();
You can use Spring Data JPA specifications:
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications
You simply have to implement the method toPredicate:
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder);
In this method you get root, query and the CriteriaBuilder to create a Criteria API where predicate.
Event methods like findAll take a Specification.

#Query not recognizing parameters if they are inside single quote in Spring Framework

I have a big problem with Spring Data in #Query.
I have the following query :
SELECT created_at::DATE "date",
count(*)
FROM
absence
WHERE
created_at::DATE between '2018-05-27' AND '2018-05-31'
GROUP BY created_at::DATE;
this query works in postgres without any problem now in spring to use it :
#Query("SELECT created_at\\:\\:DATE \"date\",count(*) FROM absence " +
"WHERE created_at\\:\\:DATE between '?1' AND '?2' " +
"GROUP created_at\\:\\:DATE", nativeQuery = true)
fun getAbsenceCount(beginDate: String,endDate: String): List<AbsenceCount>
This query doesn't work at all, the problem is that spring can't recognize the parather beginDate (?1) & endDate (?2).
I tried a lot of solution from stackoverflow like solution and solution 2 but I can't get rid of this problem.
I don't know if it's intented or it's a bug in spring.
Try to simply use the classic SQL cast function:
#Query(value = "" +
"select " +
" cast(a.created_at as date) as createdAt, " +
" count(*) as quantity " +
"from " +
" absence a " +
"where " +
" cast(a.created_at as date) between ?1 and ?2 " +
"group " +
" cast(a.created_at as date)" +
"", nativeQuery = true)
fun getAbsenceCount(beginDate: String, endDate: String): List<AbsenceCount>
where AbsenceCount is a projection like this (java):
public interface AbsenceCount {
LocalDate getCreatedAt();
Long getQuantity();
}

Alias Column In Resultset

I have a query in backend spring project, where I create a field alias named agent_id but I don´t have that column in Model. What is the best approach for showing this as a field in table view in front end???.
Here´s the query:
Query query = em.createNativeQuery("SELECT" +
" ips.*, " +
" s.mls_id AS imported," +
" p.INTEGRATOR_SALES_ASSOCIATED_ID AS agent_id " +
"FROM test1.ilist_property_summary ips " +
" LEFT JOIN test1.statistics s ON s.mls_id = ips.INTEGRATOR_PROPERTY_ID " +
" LEFT JOIN test1.person p ON p.INTEGRATOR_SALES_ASSOCIATED_ID = ips.INTEGRATOR_SALES_ASSOCIATED_ID " +
"WHERE ips.AGENCY_ID = :agencyId AND (s.statistics_type = 1 OR s.statistics_type IS NULL) AND ips.ORIG_LISTING_DATE BETWEEN :startDate AND :endDate");
query.setParameter("agencyId", agencyId)
.setParameter("startDate", DateUtil.asDate(startDate), TemporalType.DATE)
.setParameter("endDate", DateUtil.asDate(endDate), TemporalType.DATE);
return (List<PropertyVO>) query.getResultList().stream().map(o -> processProperty((Object[]) o)).collect(Collectors.<PropertyVO>toList());
I already have a field that shows agent_id, but not fetched with same field of table Person. I need to retrieve that field agent id alias fetched from left join.
I followed another approach:
Query query = em.createNativeQuery("SELECT" +
" ips.id, ips.agency_id, ips.integrator_property_id, ips.orig_listing_date, ips.contract_type," +
" ips.transaction_type, ips.current_listing_price, ips.current_listing_currency, ips.apartment_number, ips.commercial_residential," +
" ips.commission_percent, ips.commission_value, ips.property_type, ips.street_name, ips.street_number," +
" s.mls_id AS imported," +
" p.INTEGRATOR_SALES_ASSOCIATED_ID AS INTEGRATOR_SALES_ASSOCIATED_ID" +
" FROM test1.ilist_property_summary ips " +
" LEFT JOIN test1.statistics s ON s.mls_id = ips.INTEGRATOR_PROPERTY_ID " +
" LEFT JOIN test1.person p ON p.INTEGRATOR_SALES_ASSOCIATED_ID = ips.INTEGRATOR_SALES_ASSOCIATED_ID " +
"WHERE ips.AGENCY_ID = :agencyId AND (s.statistics_type = 1 OR s.statistics_type IS NULL) AND ips.ORIG_LISTING_DATE BETWEEN :startDate AND :endDate ");
Here field
INTEGRATOR_SALES_ASSOCIATED_ID
is taken from table p, and not from ips. I just selected specific fields so this one could be taken from the other table.

Spring data #Query: If optional Boolean param is given check size of other String column in where clause

i trying to build a #Query in one of my repositories which gets a optional Boolean param named "hasComment" in my example. If the Boolean is given/exists i want to make a length check on annother character varying column ("comment") in my where clause.
Currently im having the following code which i didn't expect to work (and isn't of course) but it should show the way i was imagine how it could work.
#Query("SELECT t "
+ "FROM Test t "
+ "WHERE "
+ "(:from IS NULL OR t.startTs >= :from) "
+ "AND (:to IS NULL OR t.endTs <= :to) "
+ "AND ((:hasComment) IS NULL OR ("
+ "(CASE WHEN :hasComment = true THEN length(t.comment) > 0)"
+ "OR (CASE WHEN :hasComment = false THEN length(t.comment) = 0 OR t.comment IS NULL)"
+ ")"
Page<Test> find(#Param("from") Instant from, #Param("to") Instant to,
#Param("hasComment") Boolean hasComment, Pageable pageable);
Can anybody help me out? I just can't really find informations yet how to build this query and even if this is possible at all with a #Query...
I don't think it is possible to use a CASE inside a WHERE condition.
Regarding your query there is a workaround
"AND (:to IS NULL OR t.endTs <= :to) "
+ "AND ((:hasCategory) IS NULL OR ("
+ "(CASE WHEN :hasCategory = true THEN length(t.comment) > 0)"
+ "OR (CASE WHEN :hasCategory = false THEN length(t.comment) = 0 OR t.comment IS NULL)"
+ ")"
to
AND ((:hasCategory) IS NULL OR (
(:hasCategory=true AND length(t.comment)>0) OR
(:hasCategory = false AND length(t.comment) = 0) OR
(:hasCategory = false AND t.comment IS NULL))
)

Resources