Pasar la lista vacía como parámetro a la consulta JPA genera un error


Si paso una lista vacía a una consulta JPA, obtengo un error. Por ejemplo:

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 un error con la base de datos hipersónica.

Hay un ticket para esto en Hibernate issue tracking pero no hay muchos comentarios/actividad allí. Tampoco conozco el soporte en otros productos OR ni en las especificaciones de JPA.

No me gusta la idea de tener que comprobar manualmente si hay objetos nulos y listas vacías cada vez. ¿Hay algún enfoque/extensión comúnmente conocido para esto? ¿Cómo maneja estas situaciones?

Author: Tuukka Mustonen, 2010-03-22

3 answers

De acuerdo con la sección 4.6.8 En Expresiones de la especificación JPA 1.0:

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

En otras palabras, independientemente de la capacidad de Hibernate para analizar la consulta y pasar una IN(), independientemente del soporte de esta sintaxis por bases de datos particulares (PosgreSQL no lo hace de acuerdo con el problema de Jira), debe usar una consulta dinámica aquí si quiero que tu código sea portable (y normalmente prefiero usar la API de Criterios para consultas dinámicas).

 29
Author: Pascal Thivent,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-21 23:22:58

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

ADVERTENCIA: Este es un trabajo en progreso y un enfoque muy peligroso. El código a continuación no es de ninguna manera significa como solución completa y muy posiblemente contiene zillions de errores y casos de miedo.

Dicho esto, la clase Blankawarezery envuelve el javax.consulta de persistencia y se inicializa con EntityManager y el núcleo cadena de consulta (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, las partes dinámicas se insertan con

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

Los parámetros se insertan como siempre:

query.setParameter("addresses", addresses);

El punto aquí es que la clase elimina estos (su from-part también) de la consulta si son listas vacías o los 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 (el código usa Lombok para @Data and @ NonNull annotations and Apache commons lang for 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
Author: Tuukka Mustonen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-23 10:13:50

Solución:

if (municipalities==null || municipalities.isEmpty())
    .setParameter("municipalities", "''")
else
    .setParameter("municipalities", municipalities)
 1
Author: Samanta,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-29 13:12:12