如何使用JPA条件API过滤postgres数组列?

时间:2014-07-11 10:08:44

标签: java spring hibernate postgresql jpa

我正在使用:

  • Hibernate 4.3.5
  • Spring JPA 1.6.0
  • Javax Persistence API 2.1

" refcodemailing" column被定义为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)

有办法吗?

3 个答案:

答案 0 :(得分:1)

我尝试了各种选择,但对我不起作用。终于明白了,如果第二个参数是内置函数的数组,就是扩展变量,转换为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);

答案 1 :(得分:1)

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

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

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

答案 2 :(得分:0)

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

它有效:

SELECT  criteriaap0_.id AS id1_0_
FROM    CalendarEvent criteriaap0_
WHERE   1 = 1
        AND ( 1 IN ( SELECT mailingcod1_.mailingCodes
                     FROM   CalendarEvent_mailingCodes mailingcod1_
                     WHERE  criteriaap0_.id = mailingcod1_.CalendarEvent_id ) )
        AND ( 2 IN ( SELECT mailingcod2_.mailingCodes
                     FROM   CalendarEvent_mailingCodes mailingcod2_
                     WHERE  criteriaap0_.id = mailingcod2_.CalendarEvent_id ) )
        AND ( 3 IN ( SELECT mailingcod3_.mailingCodes
                     FROM   CalendarEvent_mailingCodes mailingcod3_
                     WHERE  criteriaap0_.id = mailingcod3_.CalendarEvent_id ) )

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