Передача пустого списка в качестве параметра в запрос JPA вызывает ошибку

Если я передаю пустой список в запрос JPA, я получаю сообщение об ошибке. Например:

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

Поскольку список пуст, Hibernate генерирует его в SQL как «IN ()», что дает мне ошибку с базой данных Hypersonic.

Для этого есть запрос в отслеживании проблем в спящем режиме, но их немного. комментарии/активность там. Я не знаю о поддержке в других продуктах ORM или в спецификации JPA.

Мне не нравится идея каждый раз вручную проверять нулевые объекты и пустые списки. Есть ли какой-то общеизвестный подход/расширение к этому? Как вы справляетесь с такими ситуациями?


person Tuukka Mustonen    schedule 21.03.2010    source источник
comment
IN(), что выдает мне ошибку с базой данных Hypersonic. Это происходит и с MySQL.   -  person Joshua Taylor    schedule 01.10.2015


Ответы (9)


Согласно разделу 4.6.8 In Expressions из спецификации JPA 1.0:

В списке, разделенном запятыми, должен быть хотя бы один элемент, определяющий набор значений для выражения IN.

Другими словами, независимо от способности Hibernate анализировать запрос и передавать IN(), независимо от поддержки этого синтаксиса конкретными базами данных (PosgreSQL не поддерживает в соответствии с проблемой Jira), вы должны использовать здесь динамический запрос, если хотите чтобы ваш код был переносимым (и я обычно предпочитаю использовать Criteria API для динамических запросов).

person Pascal Thivent    schedule 21.03.2010
comment
В настоящее время я использую JPA 1.0, у которого нет API критериев. Я собирался перейти на JPA 2.0, но Hibernate 3.5 еще не окончательная версия, так что это не вариант. Однако с API критериев по-прежнему приходится вручную избегать нулей и пустых списков, просто это проще сделать. Пожалуйста, поправьте меня, если я ошибаюсь. JPA 1.0 существует некоторое время, я уверен, что люди придумали решения для обработки динамических запросов со спецификацией JPA 1.0? - person Tuukka Mustonen; 22.03.2010
comment
@Tuukka 1) Вы правы, в JPA 1.0 нет Criteria API 2) Действительно, Criteria API просто упрощает/чище обработку нулей, но вам все равно приходится с ними обращаться 3) Насколько я знаю, решение состоит в том, чтобы динамически построить строку запроса JPQL во время выполнения. - person Pascal Thivent; 22.03.2010
comment
@Tuukka Обратите внимание, что в вашем конкретном случае вы должны просто использовать два разных статических запроса. - person Pascal Thivent; 23.03.2010
comment
Использование Spring Data: вместо передачи пустого списка передайте null. он будет оцениваться как ложный. - person TecHunter; 14.06.2019
comment
@TecHunter, нет, он выдаст java.lang.IllegalArgumentException: значение не должно быть нулевым! если вы попытаетесь ввести ноль - person Max; 15.05.2020
comment
@Max с данными Spring? как вы его проходите? сделать еще один вопрос SO - person TecHunter; 17.05.2020

Предполагая, что запрос SQL похож на

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

Теперь, если список имеет тип String, вы можете передать его как

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

И если список содержит значения Integer, то синтаксис выглядит следующим образом:

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

person r_divyas    schedule 23.03.2019

Я тоже боролся с этой проблемой. Я узнал, что сообщество Hibernate РЕШИЛО проблему в Hibernate версии 5.4.10, вот тикет: https://hibernate.atlassian.net/browse/HHH-8091

Вы можете проверить свою версию Hibernate System.out.println(org.hibernate.Version.getVersionString());

И вы можете ОБНОВИТЬ версию Hibernate до последней, вот полезная ссылка: https://hibernate.org/orm/releases/

person Reneta    schedule 05.05.2020

Решение:

if (municipalities==null || municipalities.isEmpty())
    .setParameter("municipalities", "''")
else
    .setParameter("municipalities", municipalities)
person Samanta    schedule 05.06.2012
comment
На мой вопрос: Мне не нравится идея каждый раз вручную проверять нулевые объекты и пустые списки. - person Tuukka Mustonen; 06.06.2012
comment
в вашем случае, если вы работаете, потому что вы будете отправлять список, если запрос, который дает тот же запрос, указанный индекс, не был найден, результаты - person Samanta; 07.06.2012
comment
' ' ненадежен, тип, который вы проверяете, скажем, Long, затем он терпит неудачу с value [''] did not match expected type [java.lang.Long. В любом случае, если вы добавляете проверку в java, верните пустой список вместо запуска запроса. - person Nils; 29.04.2016

Не имея фактического решения в качестве ответов, я создал прокси-класс для обработки этих ситуаций. Идея состоит в том, чтобы по возможности сохранить родной синтаксис.

ВНИМАНИЕ: это незавершенный и очень опасный подход. Приведенный ниже код ни в коем случае не является полным решением и, вполне возможно, содержит миллионы ошибок и страшных случаев.

При этом класс BlankAwareQuery обертывает запрос javax.persistence и инициализируется с помощью EntityManager и основной строки запроса (которая не может содержать пустые списки или списки перечислений).

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

После создания класса динамические части вставляются с помощью

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

Параметры вставляются как всегда:

query.setParameter("addresses", addresses);

Дело в том, что класс удаляет их (также их часть from) из запроса, если они являются пустыми списками, или манипулирует ими, если они являются списками перечислений.

Затем позвоните:

query.getResultList();

Так, например:

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

Фактический код (код использует Lombok для аннотаций @Data и @NonNull и общий язык Apache для 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();
    }
}
person Tuukka Mustonen    schedule 23.03.2010

Сегодня я столкнулся с той же проблемой, и ответ @r_divyas почти помог мне, но было небольшое изменение, которое мне пришлось сделать, чтобы заставить его работать.

У меня есть запрос, который я сохраняю в базе данных, поэтому я не могу изменить его на лету. Этот запрос инициирует создание динамического пользовательского интерфейса в соответствии с полями в результирующем наборе.

В пользовательском интерфейсе у меня есть раскрывающийся список с несколькими вариантами выбора, который не является обязательным. Таким образом, из него может быть выбрано 0, 1 или более элементов.

Поэтому мне нужно было найти способ сделать этот параметр необязательным в моем запросе. Изначально я настроил так:

(:assignedUsers IS NULL OR wr.USR_ID IN (:assignedUsers))

К сожалению, это работало только с нулевым параметром или списком из одного элемента. На этапе проектирования я не тестировал его с несколькими элементами в списке. Когда я, наконец, добрался туда, я столкнулся со следующей ошибкой:

SQL Error [4145] [S0001]: An expression of non-boolean type specified in a context where a condition is expected, near ','.

Это было вызвано следующим кодом:

AND (('id1', 'id2', 'id3') IS NULL OR wr.USR_ID IN ('id1', 'id2', 'id3'))

Затем я попробовал его решение, которое действительно работало, когда мой список не был нулевым или пустым. Но когда это было, запрос становился таким:

AND (COALESCE('', NULL) IS NULL OR wr.USR_ID IN (''))

Что приводило к тому, что набор результатов был пустым.

А теперь перейдем к найденному мной решению. Все, что мне нужно было сделать, это изменить условие в запросе:

AND (COALESCE(:assignedUsers, NULL) = '' OR wr.USR_ID IN (:assignedUsers))

И это работает для пустого списка, нулевого значения, списка с одним или несколькими элементами.

person Marius Mihai Vintila    schedule 05.04.2021

Поскольку вы запрашиваете идентификаторы последовательности БД, которые обычно начинаются с 1, вы можете добавить 0 в список.

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

Может быть, -1 тоже работает. Небольшая работа для использования репозиториев jpa.

person user10747457    schedule 05.12.2018

Мое простое решение состояло в том, чтобы добавить вспомогательный параметр, чтобы пометить условие, как в приведенном ниже примере:


where (:doNotCheckList = 1)
   or (fieldName in :valuesList)


query
  .setParameter("doNotCheckList", (!valuesList.isEmpty() ? 1 : 0))
  .setParameter("valuesList",     valuesList);

person Antonio Petricca    schedule 20.04.2021

Если вы используете аннотации spring/hibernate, то одним из самых простых способов обойти это является наличие двух параметров. Сначала давайте определим нашу аннотацию JPA:

        + "AND (p.myVarin :state OR :nullCheckMyVar is null) "

Затем передайте эти параметры как обычно:

    @Param("myVarin") List<Integers> myVarin,
    @Param("nullCheckMyVar") Integer nullCheckMyVar,

Наконец, при любом сервисном вызове вы можете сделать следующее:

    Integer nullCheckInteger = null;
    if (myVarIn != null && !myVarIn.isEmpty()) {
        // we can use any integer
        nullCheckInteger = 1;
    }
    repo.callService(myVarIn, nullCheckInteger);

А затем передайте эти два параметра в вызов.

Что это делает? Если myVarIn не равен нулю и заполнен, тогда мы можем указать «все» как 1 != null. В противном случае мы передаем наш список, а значение null равно нулю и поэтому игнорируется. Это либо выбирает «все», когда пусто, либо что-то, когда не пусто.

person PeterS    schedule 01.11.2018