【问题标题】:How to filter a PostgreSQL array column with the JPA Criteria API?如何使用 JPA Criteria API 过滤 PostgreSQL 数组列?
【发布时间】:2014-09-01 22:24:14
【问题描述】:

我正在使用:

  • 休眠 4.3.5
  • Spring JPA 1.6.0
  • Javax 持久性 API 2.1

“refcodemailing”列被定义为一个int数组:int[]

我的实体对象:

@Entity
@Table
public class CalendarEvent implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private int id = 0;

  @Convert(converter = IntegerArrayConverter.class)
  @Column(name = "refcodemailing")
  private final List<Integer> mailingCodes = new ArrayList<>();

  // ....

}

我正在尝试使用以下 JPA 规范方法过滤列数组:

private final List<MailingCode> mailingCodes = new ArrayList<>();

@Override
public Predicate toPredicate(Root<CalendarEvent> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

  // Mailing codes
  if(!mailingCodes.isEmpty()){
    List<Predicate> mailingCodePred = new ArrayList<>();

    for(MailingCode mailingCode: mailingCodes){
      restrictions.add(cb.isMember(mailingCode.getId(), root.<List<Integer>>get("mailingCodes")));
    }

    restrictions.add(cb.and(cb.isNotNull(root.<List<Integer>>get("mailingCodes")),       cb.or(mailingCodePred.toArray(new Predicate[]{}))));
  }
}

但是抛出以下异常:

java.lang.IllegalArgumentException: unknown collection expression type [org.hibernate.jpa.criteria.path.SingularAttributePath]
    at org.hibernate.jpa.criteria.CriteriaBuilderImpl.isMember(CriteriaBuilderImpl.java:1332)
    at com.agenda.CalendarEventQuery.toPredicate(CalendarEventQuery.java:100)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.applySpecificationToCriteria(SimpleJpaRepository.java:521)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.getQuery(SimpleJpaRepository.java:472)

有办法吗?

【问题讨论】:

    标签: java spring hibernate postgresql jpa


    【解决方案1】:

    根据 JPA 2.0 规范:

    不支持计算为可嵌入类型的表达式 集合成员表达式。支持使用嵌入式 集合成员表达式可能会在此版本的未来版本中添加 规范。

    但是,我使用 Hibernate 构建了 a working example on GitHub

    假设我们有这个CalendarEvent 实体和MailingCode DTO 对象:

    @Entity(name = "CalendarEvent")
    @Table
    public static class CalendarEvent implements Serializable {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        @ElementCollection
        private final List<Integer> mailingCodes = new ArrayList<>();
    
    }
    
    public static class MailingCode {
        private Integer id;
    
        public MailingCode(Integer id) {
            this.id = id;
        }
    
        public Integer getId() {
            return id;
        }
    }
    

    您可以编写 Criteria API 代码如下:

    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    CriteriaQuery<CalendarEvent> criteria = builder.createQuery(CalendarEvent.class);
    Root<CalendarEvent> root = criteria.from(CalendarEvent.class);
    
    List<MailingCode> mailingCodes = Arrays.asList(
        new MailingCode(1),
        new MailingCode(2),
        new MailingCode(3)
    );
    
    Expression<List<Integer>> mailingCodesPath = root.get("mailingCodes");
    
    Predicate predicate = builder.conjunction();
    
    for(MailingCode mailingCode: mailingCodes){
        predicate = builder.and(predicate, builder.isMember(mailingCode.getId(), mailingCodesPath));
    }
    
    criteria.where(predicate);
    List<CalendarEvent> events = entityManager.createQuery(criteria).getResultList();
    

    但是,IN 查询是更好的选择,因为上面的 SQL 查询不是最理想的。

    【讨论】:

    • 嗨弗拉德,你的例子工作得很好,但我认为这个问题解决的问题与我在你的例子中没有看到的@Converter 有关。我说的对吗?
    • 但是问题没有显示转换器代码,这就是我选择这种方法的原因。
    • 如果你想检查它,这里有一个附加的测试用例的问题,它与转换器hibernate.atlassian.net/browse/HHH-9991有同样的问题
    • 这可能是一个错误。尝试使用自定义类型,看看它是否效果更好。
    【解决方案2】:

    我尝试了各种选项,但对我不起作用。终于明白了,如果第二个参数是内置函数的数组,就是扩展变量,转换成myVarArgMethod。所以我做了什么,我编写了自己的自定义数据库函数,如下所示。

    如果 searchKey 是单值我们可以使用 arrayContains,如果 searchKey 包含多个值,我们可以使用 java util 函数将列表转换为 postrgress 数组格式字符串,在 postgres 函数中,我们可以通过类型转换将其转换为数组。

    用于将列表转换为 postgress 数组字符串以及反之亦然的 Java util 方法

    public static String convertToPGArray(List<String> content){
            StringBuilder str = new StringBuilder();
            if(content != null){
                str.append("{");
                int counter = 0;
                for(String text : content){
                    if(counter != 0){
                        str.append(",");
                        counter++;
                    }else{
                        counter++;
                    }
                    str.append("\"").append(text).append("\"");
                }
                str.append("}");
            }else{
                str.append("{}");
            }
            return str.toString();
        }
    
        public static List<String> convertToList(String content){
            List<String> returnList = new ArrayList<>();
            if(!(content == null || content.equals("{}") || content.trim().equals(""))){
                String tempContent = content;
    
                String[] tokens = tempContent.replace("{", "").replace("}", "").split(",");
                returnList = Arrays.stream(tokens).collect(Collectors.toList());
            }
    
            return returnList;
        }
    

    arrayContains 和 arrayContainsAny 的自定义 postgres 函数

    CREATE OR REPLACE FUNCTION arrayContains(arrayContent text[], searchKey text) RETURNS BOOLEAN as
    '
    DECLARE arrContent text[];
    countVal integer :=0;
    BEGIN
        arrContent = $1::text[];
    
        countVal = (SELECT count(array_position(arrContent, searchKey)));
        IF countVal = 0 THEN
            RETURN FALSE;
        ELSE
            RETURN TRUE;
        END IF;
    
    EXCEPTION WHEN others THEN
        RETURN FALSE;
    END;'
    LANGUAGE plpgsql;
    
    CREATE OR REPLACE FUNCTION arrayContainsAny(arrayContent text[], searchKeys text) RETURNS BOOLEAN as
    '
    DECLARE arrContent text[];
    DECLARE serKeys text[];
    countVal integer :=0;
    result boolean;
    searchkey text;
    BEGIN
        arrContent = $1::text[];
        serKeys = $2::text[];
    
        IF (count(cardinality(arrContent)) = 0 OR count(cardinality(serKeys)) = 0 OR cardinality(arrContent) = 0 OR cardinality(serKeys) = 0) THEN
            RAISE NOTICE $quote$array is null$quote$;
            RETURN TRUE;
        END IF;
    
        RAISE NOTICE $quote$after if condition$quote$;
    
        FOREACH searchkey IN ARRAY serKeys
        LOOP
            result = arrayContains(arrContent, searchkey);
            IF result = true THEN
                RETURN TRUE;
            END IF;
        END LOOP;
    
        RETURN FALSE;
    
    EXCEPTION WHEN others THEN
        RAISE NOTICE $quote$exception$quote$;
        RETURN FALSE;
    END;'
    LANGUAGE plpgsql;
    

    我们可以在 QueryBuilder 或 @Query 注释中调用上面的函数,如下所示

    如果 QueryBuilder 示例输出如下所示

    Specification<T> siteReqSpec1 = new Specification<T>() {
                                @Override
                                public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                                    // TODO Auto-generated method stub
                                    logger.info("getRBACResourceTagSpec entityName {} value {}", root.getModel().getName(), root.get(colName).getJavaType());
                                    //return cb.isNotNull(root.get(colName));
                                    //return cb.isMember(roleTagName, root.get(colName));
                                    return cb.or(cb.isNull(root.get(colName)),
                                            cb.isTrue(cb.function("arrayContains", Boolean.class, root.get(colName), cb.literal(roleTagName))));
                                }
    
                            };
    

    如果是@Query 注释,如下所示

    @Query("from DeviceworkFlowLite wf where wf.orgName = :organization and arrayContainsAny(rbac_resource_tags, :rbacResourceTags) = true")
        Page<DeviceworkFlowLite> findAllByOrgNameAndRBACResourceTagsIn(@Param("organization")String organization,  @Param("rbacResourceTags")String rbacResourceTags, Pageable pageable);
    

    如果是jdbc SQL语句

        private static final String GET_ALL_TEMPLATE_FILTER_BY_ORG = 
                "select name,rbac_resource_tags from template_metadata "
    
                + " and arrayContainsAny(rbac_resource_tags, ?) = true ";
       ps.setString(4, jsonArrayRBACRoleResourceTag);
    

    【讨论】:

      【解决方案3】:

      i v s narayana 把我带到了那里。我为我的用例简化了他们的答案,只使用了内置的 sql 函数。

      检查 valueName 是否是存储在 columnName 中的 sql 数组的成员:

                  cb.isNotNull(cb.function("array_position", Integer.class, root.get(columnName), cb.literal(valueName))),
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-11-20
        • 2018-07-04
        • 1970-01-01
        • 1970-01-01
        • 2014-11-20
        • 1970-01-01
        • 1970-01-01
        • 2021-12-22
        相关资源
        最近更新 更多