web-dev-qa-db-fra.com

La transmission d'une liste vide en tant que paramètre à la requête JPA génère une erreur

Si je passe une liste vide dans une requête JPA, j'obtiens une erreur. Par exemple:

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();

Parce que la liste est vide, Hibernate génère cela dans SQL comme "IN ()", ce qui me donne une erreur avec la base de données Hypersonic.

Il y a un ticket pour cela dans Hibernate issue tracking mais il n'y a pas beaucoup de commentaires/activités. Je ne sais pas non plus sur le support dans d'autres produits ORM ou dans les spécifications JPA.

Je n'aime pas l'idée d'avoir à vérifier manuellement les objets nuls et les listes vides à chaque fois. Existe-t-il une approche/extension connue à ce sujet? Comment gérez-vous ces situations?

30
Tuukka Mustonen

Selon la section 4.6.8 In Expressions de la spécification JPA 1.0:

Il doit y avoir au moins un élément dans la liste séparée par des virgules qui définit l'ensemble de valeurs pour l'expression IN.

En d'autres termes, quelle que soit la capacité d'Hibernate à analyser la requête et à transmettre une IN(), quelle que soit la prise en charge de cette syntaxe par des bases de données particulières (PosgreSQL ne le fait pas selon le problème Jira), vous devez utiliser un requête dynamique ici si vous souhaitez que votre code soit portable (et je préfère généralement utiliser l'API Criteria pour les requêtes dynamiques).

36
Pascal Thivent

Solution:

if (municipalities==null || municipalities.isEmpty())
    .setParameter("municipalities", "''")
else
    .setParameter("municipalities", municipalities)
1
Samanta

En supposant que la requête SQL ressemble à

(COALESCE(:placeHolderName,NULL) IS NULL OR Column_Name in (:placeHolderName))

Maintenant, si la liste est de type String, vous pouvez passer comme

query.setParameterList("placeHolderName", 
!CollectionUtils.isEmpty(list)? list : new ArrayList<String>(Arrays.asList("")).

Et si la liste contient les valeurs Integer, la syntaxe est comme ci-dessous:

If(!CollectionUtils.isEmpty(list)){
query.setParameterList("placeHolderName",list)
}else{
query.setParameter("placeHolderName",null, Hibernate.INTEGER)
}

1
r_divyas

Après n'avoir aucune solution réelle en tant que réponses, j'ai créé une classe proxy pour gérer ces situations. L'idée est de conserver la syntaxe native lorsque cela est possible.

AVERTISSEMENT: Il s'agit d'une approche en cours de travail et très dangereuse. Le code ci-dessous n'est en aucun cas une solution complète et contient très probablement des millions de bugs et de cas effrayants.

Cela étant dit, la classe BlankAwareQuery encapsule la requête javax.persistence et est initialisée avec EntityManager et la chaîne de requête principale (qui ne peut pas contenir de listes vides ou d'énumérations).

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

Après la création de la classe, les pièces dynamiques sont insérées avec

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

Les paramètres sont insérés comme toujours:

query.setParameter("addresses", addresses);

Le point ici est que la classe les supprime (leur partie de départ également) de la requête si ce sont des listes vides ou les manipule si ce sont des listes d'énumérations.

Puis appelez:

query.getResultList();

Ainsi, par exemple:

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();

Le code réel (le code utilise Lombok pour les annotations @Data et @NonNull et Apache commons lang pour 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
Tuukka Mustonen

Étant donné que vous interrogez des ID de séquence DB qui commencent généralement à 1, vous pouvez ajouter 0 à la liste

if (excludeIds.isEmpty()) {
    excludeIds.add(new Long("0"));
}
List<SomeEntity> retval = someEntityRepo.findByIdNotIn(excludeIds);

Peut-être que -1 fonctionne aussi. Petit travail autour de l'utilisation des dépôts jpa.

0
user10747457