代码之家  ›  专栏  ›  技术社区  ›  Tuukka Mustonen

将空列表作为参数传递给JPA查询引发错误

  •  22
  • Tuukka Mustonen  · 技术社区  · 15 年前

    如果我将一个空列表传递到一个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()”,这给了我有关高超音速数据库的错误。

    有张这个的票在 Hibernate issue tracking 但是没有太多的评论/活动。我也不知道其他ORM产品或JPA规范中的支持。

    我不喜欢每次都必须手动检查空对象和空列表的想法。是否有一些常见的方法/扩展?你如何处理这些情况?

    5 回复  |  直到 6 年前
        1
  •  33
  •   Pascal Thivent    15 年前

    根据章节 4.6.8表达式 根据JPA 1.0规范:

    逗号分隔列表中必须至少有一个元素定义 IN 表达式。

    换句话说,不管Hibernate解析查询和传递 IN() ,不管特定数据库是否支持这种语法(posgresql不符合jira的问题),如果您希望代码可移植,那么应该在这里使用动态查询(我通常更喜欢使用标准API进行动态查询)。

        2
  •  1
  •   Tuukka Mustonen    15 年前

    在没有实际的解决方案作为答复之后,我创建了一个代理类来处理这些情况。其思想是尽可能保留本机语法。

    警告: 这是正在进行的工作,而且是非常危险的方法。下面的代码并不是完整的解决方案,很可能包含无数的错误和可怕的情况。

    也就是说,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);
    

    这里的要点是,如果它们是空列表,那么类会从查询中删除它们(它们也是从部分中删除的),如果它们是枚举列表,则会对它们进行操作。

    然后呼叫:

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

    实际代码(代码对@data和@nonnull注释使用lombok,对stringutils使用apache commons lang):

    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();
        }
    }
    
        3
  •  1
  •   Alexey Romanov    12 年前

    解决方案:

    if (municipalities==null || municipalities.isEmpty())
        .setParameter("municipalities", "''")
    else
        .setParameter("municipalities", municipalities)
    
        4
  •  -1
  •   PeterS    6 年前

    如果您使用的是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不为空且已填充,则可以将“all”指定为1!= NULL。否则,我们将传入列表,空值为空,因此被忽略。 空的时候选择“全部”,不空的时候选择“全部”。

        5
  •  -1
  •   user10747457    6 年前

    由于查询的数据库序列ID通常从1开始,因此可以将0添加到列表中。

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

    也许-1也可以。为使用JPA回购而进行的小型工作。

    推荐文章