13

I got this list of coins that i want to filter with that 3 optional parameters(Currency, quantity and year)

How can I set parameters in JPQL as optional? I dont want to do 9 "if else" for checking if its null

I got the function filtradoMonedas (filterCoins) that filter the object Moneda(coin) using that 3 OPTIONAL parameters but its not working if there´s a null parameter.

This just work well if dont set empty parameters, if cantidad or ano is "" returns exception of bad query. Just want it as an optional. Heres the method:

public List<Moneda> filtradoMonedas(Divisa divisa, BigDecimal cantidad, 
        BigDecimal ano){

    EntityManager em = getEntityManager();

    Query consulta = em.createQuery("SELECT m FROM Moneda m "
            + "WHERE m.divisa = :divisa "
            + "AND m.cantidad= :cantidad "
            + "AND m.ano = :ano");

    consulta.setParameter("divisa", divisa);
    consulta.setParameter("cantidad", cantidad);
    consulta.setParameter("ano", ano);

    List<Moneda> filtradoMonedas = (List<Moneda>) consulta.getResultList();
    // sincronizar los objetos devueltos con la base de datos
    for (Moneda m : filtradoMonedas) {
        em.refresh(m);
    }

    em.close();
    return filtradoMonedas;
}
Nacho Martin
  • 131
  • 1
  • 1
  • 3

4 Answers4

23

After reading Ilya Dyoshin's comment, and the smart thinking behind "you should think that not parameters are optional but rather conditions are optional" I decided to go my own way by using JPQL with @Query annotation, and creating one dynamic SQL query which works beautifully.

The key is to apply some SQL logic to make the conditions optional rather than the parameters:

    @Query("SELECT a " +
           "FROM amazing " +
           "WHERE (:filterSuperAwesome IS NULL " +
                        "OR a.filterSuperAwesome = :filterSuperAwesome)"); 

    List<FilterSuperAwesome> filterAwesomeORSuperAwesome(
                    @Param("filterSuperAwesome ") FilterSuperAwesome filterSuperAwesome);

Note how here ☝️, I use an OR statement based on the fact that my parameter can present two forms, an instance of FilterSuperAwesome or NULL. When NULL, the condition is always True, as if it wasn't there.

This works just fine inside a JpaRepository class of a JHipster project.

JesusIniesta
  • 10,412
  • 1
  • 35
  • 28
  • Nice! Clever solution. I'm in similar situation and ended up with some ugly code with lots of `if` statements. This should do the trick. – Christopher Schneider Dec 13 '19 at 17:38
  • 4
    We used to use this solution for a while, until we did a load test and realized that this for some reason decreases the performance of the query considerably for cases where the optional parameters are not used. (Big table, few million entries, DB2, about four/five optional parameters). – Bernhard Mar 06 '20 at 07:56
  • @Bernhard can you give more information on that? Have you found out the reason for this behavior? – Wecherowski May 23 '20 at 10:39
  • 1
    @Wecherowski Sadly no, I'm not that familar with DB internals, it's just sth. we observed and rewriting the queries using native Queries where you can have actual optional parameters increased the perfomance significantly. – Bernhard May 24 '20 at 11:05
  • This might work with = as it just evaluates to false, however, it doesn't work with IN as IN operator doesn't allow either empty list or null on the values side (atleast in postgres), which is what I asked Google and it brought me here :-D – Teddy Oct 15 '21 at 06:13
  • @Teddy I answered the question with a solution for the IN operator. :) – Xfox Nov 05 '21 at 16:00
  • Same as @Bernhard: Bad perfs using this technique. – A. Parolini Apr 13 '22 at 09:49
  • Seconding what @Bernhard and A._Parolini are saying about performance. I've lost weeks trying to resolve this, but somewhere in the Spring/Hibernate/JPA/JDBC pipeline introducing a bunch of these conditionals creates some sort of leak. Even though the query itself, if executed directly against the SQL server, performs fine, when executed through JPA it will take 5+ seconds to return a result, and often takes an additional several seconds to return the connection to the pool. – Nicky Jan 19 '23 at 15:26
4

JPQL doesn't supports optional parameters.

Actually you should think that not parameters are optional but rather conditions are optional. This idea will lead you to creating "dynamic" queries. And this in place will lead you next to switch from JPQL to Criteria API. And this will lead you to writting something like this:

    // Actually can be generated during build, and thus can be ommited
    @StaticMetamodel(Moneda.class)
    abstract class Moneda_ {
        public static volatile SingularAttribute<Moneda, BigDecimal> cantidad;
        public static volatile SingularAttribute<Moneda, Divisia> divisia;
        public static volatile SingularAttribute<Moneda, BigDecimal> ano;
    }

    final CriteriaBuilder cb = em.getCriteriaBuilder();

    final CriteriaQuery<Moneda> cq = cb.createQuery(Moneda.class);
    final Root<Moneda> root = cq.from(Moneda.class);

    Set<Predicate> predicates = new HashSet<>(3);
    if (cantidad != null) {
        predicates.add(cb.equal(root.get(Moneda_.cantidad), cantidad));
    }

    if (ano != null) {
        predicates.add(cb.equal(root.get(Moneda_.ano), ano));
    }

    if (divisia != null) {
        predicates.add(cb.equal(root.get(Moneda_.divisia), divisia));
    }

    cq.where(predicates.toArray(new Predicate[predicates.size()]));

    em.createQuery(cq).getResultList();

    // and do whatever you want 
Ilya Dyoshin
  • 4,459
  • 1
  • 19
  • 18
3

In my case I had the problem that my optional parameter was a List<String> and the solution was the following:

@Query(value = "SELECT *
                FROM ...
                 WHERE (COLUMN_X IN :categories OR COALESCE(:categories, null) IS NULL)"
, nativeQuery = true)
List<Archive> findByCustomCriteria1(@Param("categories") List<String> categories);

This way:

  • If the parameter has one or more values it is selected by the left side of the OR operator
  • If the parameter categories is null, meaning that i have to select all values for COLUMN_X, will always return TRUE by the right side of the OR operator

Why COALESCE and why a null value inside of it?

Let's explore the WHERE clause in all conditions:

Case 1: categories = null

(COLUMN_X IN null OR COALESCE(null, null) IS NULL)

The left part of the OR will return false, while the right part of the OR will always return true, in fact COALESCE will return the first non-null value if present and returns null if all arguments are null.

Case 2: categories = ()

(COLUMN_X IN null OR COALESCE(null, null) IS NULL)

JPA will automatically identify an empty list as a null value, hence same result of Case 1.

Case 3: categories = ('ONE_VALUE')

(COLUMN_X IN ('ONE_VALUE') OR COALESCE('ONE_VALUE', null) IS NULL)

The left part of the OR will return true only for those values for which COLUMN_X = 'ONE_VALUE' while the right part of the OR will never return true, because it is equals to 'ONE_VALUE' IS NULL (that is false).

Why the null as second parameter? Well, that's because COALESCE needs at least two parameters.

Case 4: categories = ('ONE_VALUE', 'TWO_VALUE')

(COLUMN_X IN ('ONE_VALUE', 'TWO_VALUE') OR COALESCE('ONE_VALUE', 'TWO_VALUE', null) IS NULL)

As in Case 3, the left part of the OR operator will select only the rows for which COLUMN_X is equale to 'ONE_VALUE' or 'TWO_VALUE'.

Xfox
  • 174
  • 2
  • 10
  • Have you checked this against MSSQL for example? From my experience this could work for Oracle and PostgreSQL but for MSSQL if will fail as it does not allow nulls only in coalesce. Just call `select coalesce(null, null)` to check. – kulas Jan 21 '22 at 09:55
  • Hi @kulas, no I dind't try it against MSSQL, only Oracle. – Xfox Jan 22 '22 at 11:09
  • I have tried this approach but the code breaks when a null value or an empty list is passed, saying "org.hibernate.exception.SQLGrammarException: could not extract ResultSet". This is happening because of the first condition where he tries to check if COLUMN_X is in null. Anyone else with this issue? – Hugo Vinhal Apr 14 '22 at 15:26
  • That's odd, are you sure your final Object correctly maps the result you are trying to extract? I suggest to google the error you're getting. – Xfox Apr 16 '22 at 06:34
  • For SQLServer you can use below : For List : ( in (:categories) or COALESCE(:categories, 'NULL') ='NULL') ) For List ( in (:employeeIds) or COALESCE(:employeeIds, -1) =-1) ) For list of integers make sure to set a fixed value as per your columns data – Abdul Mohsin Jun 02 '22 at 07:01
0

this for native queries, Simply pass null values when parameter values need to be optional. check whether the parameter is null or not in the query.

"SELECT m FROM Moneda m WHERE"
+ "(:divisa is null || m.divisa = :divisa) "
+ "AND (:cantidad is null || m.cantidad= :cantidad) "
+ "AND (:ano is null || m.ano = :ano)"
Tiran
  • 31
  • 1
  • 4