如何使用 JPA Criteria API 过滤 PostgreSQL 数组列?

Réd*_*oui 8 java postgresql spring hibernate jpa

我在用:

  • 休眠 4.3.5
  • 春季 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<>();

  // ....

}
Run Code Online (Sandbox Code Playgroud)

我正在尝试使用以下 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[]{}))));
  }
}
Run Code Online (Sandbox Code Playgroud)

但是抛出以下异常:

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)
Run Code Online (Sandbox Code Playgroud)

有没有办法做到这一点?

小智 10

ivs narayana让我到达那里。我针对我的用例简化了他们的答案,只使用了内置的 sql 函数。

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

            cb.isNotNull(cb.function("array_position", Integer.class, root.get(columnName), cb.literal(valueName))),
Run Code Online (Sandbox Code Playgroud)


Vla*_*cea 5

根据 JPA 2.0 规范:

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

但是,我使用 Hibernate在 GitHub 上构建了一个工作示例

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

@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;
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以按如下方式编写 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();
Run Code Online (Sandbox Code Playgroud)

然而,IN 查询是一个更好的选择,因为上面的 SQL 查询是次优的。


小智 5

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

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

Java util 方法将列表转换为 postgress 数组字符串,反之亦然

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;
    }
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

我们可以在 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))));
                            }

                        };
Run Code Online (Sandbox Code Playgroud)

如果使用 @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);
Run Code Online (Sandbox Code Playgroud)

如果是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);
Run Code Online (Sandbox Code Playgroud)