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

Posted

技术标签:

【中文标题】将空列表作为参数传递给 JPA 查询会引发错误【英文标题】:Passing empty list as parameter to JPA query throws error 【发布时间】:2011-01-30 03:25:51 【问题描述】:

如果我将一个空列表传递给 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 数据库错误。

Hibernate issue tracking 有一张票,但那里没有多少 cmets/activity。我也不知道其他 ORM 产品或 JPA 规范中的支持。

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

【问题讨论】:

""IN ()",这给了我 Hypersonic 数据库错误。 mysql 也会发生这种情况。 【参考方案1】:

我也在这个问题上苦苦挣扎。我发现 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/

【讨论】:

【参考方案2】:

我的简单解决方案是添加一个 aux 参数来标记条件,如下例所示:


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


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

【讨论】:

【参考方案3】:

我今天遇到了同样的问题,@r_divyas's response 几乎为我解决了这个问题,但我必须做一些小改动才能使其正常工作。

我有一个保存在数据库中的查询,因此我无法即时更改它。此查询根据结果集中的字段触发动态 UI 的构建。

在 UI 中,我有一个多项选择下拉菜单,这是可选的。因此可以从中选择 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))

这适用于空列表、空值、具有单个或多个元素的列表。

【讨论】:

【参考方案4】:

假设SQL查询是这样的

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

现在,如果 List 是 String 类型,那么您可以传递为

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

如果列表包含整数值,则语法如下:

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


【讨论】:

【参考方案5】:

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

if (excludeIds.isEmpty()) 
    excludeIds.add(new Long("0"));

List<SomeEntity> retval = someEntityRepo.findByIdNotIn(excludeIds);

也许 -1 也可以。使用 jpa 存储库的小工作。

【讨论】:

【参考方案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 不为 null 且已填充,那么我们可以将“all”指定为 1 != null。否则,我们传入我们的列表,并且 null 为 null,因此被忽略。 这要么在为空时选择“全部”,要么在不为空时选择某些内容。

【讨论】:

【参考方案7】:

解决方案:

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

【讨论】:

在我的问题中:我不喜欢每次都必须手动检查空对象和空列表的想法 在你的情况下,如果你工作是因为你会发送列表,如果一个查询产生相同的查询,这些索引指定的结果没有找到结果 " ' ' " 不可靠,您要检查的类型是 Long,然后它会以 value [''] did not match expected type [java.lang.Long 失败。无论如何,如果您在 java 中添加检查,则返回空列表而不是触发查询。【参考方案8】:

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

警告:这是正在进行中的非常危险的方法。下面的代码绝不是完整的解决方案,很可能包含无数的错误和可怕的案例。

话虽如此,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();

实际代码(代码使用 Lombok 表示 @Data 和 @NonNull 注释,使用 Apache commons lang 表示 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();
    

【讨论】:

【参考方案9】:

根据 JPA 1.0 规范中的 4.6.8 In Expressions 部分:

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

换句话说,无论 Hibernate 解析查询和传递IN() 的能力如何,无论特定数据库是否支持这种语法(PosgreSQL 不根据 Jira 问题),您都应该使用动态如果您希望代码可移植,请在此处查询(我通常更喜欢使用 Criteria API 进行动态查询)。

【讨论】:

我目前正在使用没有标准 API 的 JPA 1.0。我打算升级到 JPA 2.0,但 Hibernate 3.5 还不是最终版本,所以这不是选项。然而,使用标准 API 仍然需要手动避免空值和空列表,这样做更容易。如果我错了,请纠正我。 JPA 1.0 已经有一段时间了,我敢肯定人们已经提出了使用 JPA 1.0 规范处理动态查询的解决方案? @Tuukka 1) 你是对的,JPA 1.0 中没有 Criteria API 2) 事实上,Criteria API 只是让处理空值更容易/更干净,但你仍然必须处理它们 3) AFAIK,解决方案就是在运行时动态构建JPQL查询字符串。 @Tuukka 请注意,在您的特定情况下,您应该只使用两个不同的静态查询。 使用 Spring Data:不要传递空列表,而是传递 null。它将评估为假。 @TecHunter,不,它会抛出 java.lang.IllegalArgumentException: Value must not be null!如果您尝试输入 null

以上是关于将空列表作为参数传递给 JPA 查询会引发错误的主要内容,如果未能解决你的问题,请参考以下文章

将空值作为参数传递给方法时如何修复 NullPointerException [重复]

Excel Power Query:将参数传递给 oDataFeed URL 会引发错误

Delphi:如何将列表作为参数传递给SQL查询?

如何将空参数传递给 msdeploy powershell deploy 命令

在 Visual Studio 2008 中使用设计器将逗号分隔列表作为参数传递给 db2 查询的 IN 子句

将查询作为参数传递给 udf 函数