2010-03-21 50 views
20

Si paso una lista vacía en una consulta JPA, aparece un error. Por ejemplo:Pasar la lista vacía como parámetro a la consulta JPA arroja el error

List<Municipality> municipalities = myDao.findAll(); // returns empty list 
em.createQuery("SELECT p FROM Profile p JOIN p.municipality m WHERE m IN (:municipalities)") 
    .setParameter("municipalities", municipalities) 
    .getResultList(); 

Debido a que la lista está vacía, Hibernate genera esto en SQL como "IN()", lo que me da error en la base de datos Hypersonic.

Hay un boleto para esto en Hibernate issue tracking pero no hay muchos comentarios/actividades allí. No sé sobre soporte en otros productos ORM o en la especificación JPA tampoco.

No me gusta la idea de tener que verificar manualmente los objetos nulos y las listas vacías cada vez. ¿Hay alguna aproximación/extensión comúnmente conocida para esto? ¿Cómo manejas estas situaciones?

+1

"" IN() ", que me da error con la base de datos Hypersonic." Esto sucede con MySQL también. –

Respuesta

22

De acuerdo con la sección 4.6.8 en expresiones partir de la especificación JPA 1.0:

Debe haber al menos un elemento en la lista separada por comas que define el conjunto de valores para la expresión IN separó.

En otras palabras, independientemente de la capacidad de Hibernate para analizar la consulta y para pasar un IN(), independientemente del apoyo de esta sintaxis de bases de datos particulares (PosgreSQL no de acuerdo con el tema Jira), se debe utilizar una consulta dinámica aquí si desea que su código sea portátil (y generalmente prefiero usar la API de criterios para consultas dinámicas).

+0

Actualmente estoy usando JPA 1.0 que no tiene la API de criterios. Iba a actualizar a JPA 2.0, pero Hibernate 3.5 todavía no es definitivo, por lo que no es una opción. Sin embargo, con la API de criterios uno tiene que evitar nulos y listas vacías manualmente, simplemente es más fácil hacerlo. Por favor corrígeme si estoy equivocado. Después de JPA 1.0, estoy seguro de que la gente ha encontrado soluciones para manejar consultas dinámicas con la especificación JPA 1.0. –

+0

@Tuukka 1) Tiene razón, no API Criteria en JPA 1.0 2) De hecho, la API Criteria simplemente hace que manejar nulos sea más fácil/más limpio, pero aún tiene que manejarlos 3) AFAIK, la solución es construir la cadena de consulta JPQL dinámicamente en tiempo de ejecución. –

+0

@Tuukka Tenga en cuenta que en su caso particular, debe usar dos consultas estáticas diferentes. –

1

Después de no tener una solución real como respuestas, creé una clase proxy para manejar estas situaciones. La idea es retener la sintaxis nativa cuando sea posible.

ADVERTENCIA: Este es un método de trabajo en progreso y muy peligroso. El siguiente código no significa en absoluto una solución completa y muy posiblemente contiene millones de errores y casos de miedo.

Dicho esto, la clase BlankAwareQuery ajusta la consulta javax.persistence y se inicializa con EntityManager y la cadena de consulta principal (que no puede contener listas vacías o listas de enumeraciones).

BlankAwareQuery query = new BlankAwareQuery(em, "SELECT p FROM Profile p"); 

Después de la creación de la clase, partes dinámicas se insertan con

query.from("p.address a"); 
query.where("a IN (:addresses)"); 

parámetros se insertan como siempre:

query.setParameter("addresses", addresses); 

El punto aquí es que la clase elimina estos (su de- parte también) de la consulta si son listas vacías o las manipula si son listas de enumeraciones.

Entonces llama:

query.getResultList(); 

Así, por ejemplo:

List<Profile> profiles = new BlankAwareQuery(em, "SELECT p FROM Profile p") 
    .from("p.address a JOIN a.municipality m").where("m IN (:municipalities)") 
    .where("p.gender IN (:genders)") 
    .where("p.yearOfBirth > :minYear") 
    .where("p.yearOfBirth < :maxYear") 
    .from("p.platforms f").where("f IN (:platforms)") 
    .setParameter("municipalities", municipalities) 
    .setParameter("genders", genders) 
    .setParameter("minYear", minYear) 
    .setParameter("maxYear", maxYear) 
    .setParameter("platforms", platforms) 
    .getResultList(); 

El código real (código utiliza Lombok para @Data y anotaciones @NonNull y Apache Commons Lang para StringUtils):

public class BlankAwareQuery { 

    private @Data class Parameter { 
     private @NonNull String fieldName; 
     private @NonNull Object value; 
    } 

    private @Data class ClausePair { 
     private @NonNull String from; 
     private @NonNull String where; 
    } 

    private EntityManager em; 

    private List<String> select = Lists.newArrayList(); 
    private List<ClausePair> whereFrom = Lists.newArrayList(); 
    private String from; 
    private List<Parameter> parameters = Lists.newArrayList(); 
    Query query; 

    public BlankAwareQuery(EntityManager em, String query) { 

     this.em = em; 

     /** Select **/ 
     int selectStart = StringUtils.indexOf(query, "SELECT ") + 7; 
     int selectEnd = StringUtils.indexOf(query, " FROM "); 
     select(StringUtils.substring(query, selectStart, selectEnd)); 

     /** From **/ 
     int fromStart = selectEnd + 6; 
     int fromEnd = StringUtils.indexOf(query, " WHERE "); 
     if (fromEnd == -1) fromEnd = query.length(); 
     from(StringUtils.substring(query, fromStart, fromEnd)); 

     /** Where **/ 
     String where = ""; 
     if (StringUtils.contains(query, " WHERE ")) { 
      where = StringUtils.substring(query, fromEnd + 7); 
     } 
     where(where); 
    } 

    private BlankAwareQuery select(String s) { 
     select.add(s); 
     return this; 
    } 

    public BlankAwareQuery from(String s) { 
     from = s; 
     return this; 
    } 

    public BlankAwareQuery where(String s) { 
     ClausePair p = new ClausePair(from, s); 
     whereFrom.add(p); 
     from = ""; 
     return this; 
    } 

    public BlankAwareQuery setParameter(String fieldName, Object value) { 

     /** Non-empty collection -> include **/ 
     if (value != null && value instanceof List<?> && !((List<?>) value).isEmpty()) { 

      /** List of enums -> parse open (JPA doesn't support defining list of enums as in (:blaa) **/ 
      if (((List<?>) value).get(0) instanceof Enum<?>) { 

       List<String> fields = Lists.newArrayList(); 

       /** Split parameters into individual entries **/ 
       int i = 0; 
       for (Enum<?> g : (List<Enum<?>>) value) { 
        String fieldSingular = StringUtils.substring(fieldName, 0, fieldName.length() - 1) + i; 
        fields.add(":" + fieldSingular); 
        parameters.add(new Parameter(fieldSingular, g)); 
        i++; 
       } 

       /** Split :enums into (:enum1, :enum2, :enum3) strings **/ 
       for (ClausePair p : whereFrom) { 
        if (p.getWhere().contains(":" + fieldName)) { 
         int start = StringUtils.indexOf(p.getWhere(), ":" + fieldName); 
         int end = StringUtils.indexOfAny(StringUtils.substring(p.getWhere(), start + 1), new char[] {')', ' '}); 
         String newWhere = StringUtils.substring(p.getWhere(), 0, start) + StringUtils.join(fields, ", ") + StringUtils.substring(p.getWhere(), end + start + 1); 
         p.setWhere(newWhere); 
        } 
       } 
      } 
      /** Normal type which doesn't require customization, just add it **/ 
      else { 
       parameters.add(new Parameter(fieldName, value)); 
      } 
     } 

     /** Not to be included -> remove from and where pair from query **/ 
     else { 
      for (Iterator<ClausePair> it = whereFrom.iterator(); it.hasNext();) { 
       ClausePair p = it.next(); 
       if (StringUtils.contains(p.getWhere(), fieldName)) { 
        it.remove(); 
       } 
      } 
     } 

     return this; 
    } 

    private String buildQueryString() { 

     List<String> from = Lists.newArrayList(); 
     List<String> where = Lists.newArrayList(); 

     for (ClausePair p : whereFrom) { 
      if (!p.getFrom().equals("")) from.add(p.getFrom()); 
      if (!p.getWhere().equals("")) where.add(p.getWhere()); 
     } 

     String selectQuery = StringUtils.join(select, ", "); 
     String fromQuery = StringUtils.join(from, " JOIN "); 
     String whereQuery = StringUtils.join(where, " AND "); 

     String query = "SELECT " + selectQuery + " FROM " + fromQuery + (whereQuery == "" ? "" : " WHERE " + whereQuery); 

     return query; 
    } 

    public Query getQuery() { 
     query = em.createQuery(buildQueryString()); 
     setParameters(); 
     return query; 
    } 

    private void setParameters() { 
     for (Parameter par : parameters) { 
      query.setParameter(par.getFieldName(), par.getValue()); 
     } 
    } 

    public List getResultList() { 
     return getQuery().getResultList(); 
    } 

    public Object getSingleResult() { 
     return getQuery().getSingleResult(); 
    } 
} 
1

Solución:

if (municipalities==null || municipalities.isEmpty()) 
    .setParameter("municipalities", "''") 
else 
    .setParameter("municipalities", municipalities) 
+4

En mi pregunta: _No me gusta la idea de tener que verificar manualmente los objetos nulos y las listas vacías cada vez_. –

+0

en su caso si trabaja porque va a enviar la lista, si no se encontró una consulta que arroje la misma consulta, este índice no se encontró resultados – Samanta

+0

"''" no es confiable, el tipo que está buscando es, por ejemplo, Largo, luego falla con '' value [''] no coincide con el tipo esperado [java.lang.Long''. De todos modos, si está agregando cheque en java, devuelva la lista vacía en lugar de ejecutar una consulta. – Nils

Cuestiones relacionadas