How to filter postgres array column with the JPA criteria API?

大憨熊 提交于 2021-02-18 23:01:48

问题


I am using:

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

The "refcodemailing" column is defined as an array of int: int[]

My entity object:

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

  // ....

}

I am trying to filter the column array with the following JPA Specification method:

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[]{}))));
  }
}

But the following exception is thrown:

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)

Is there a way to do it?


回答1:


According to JPA 2.0 specs:

Expressions that evaluate to embeddable types are not supported in collection member expressions. Support for use of embeddables in collection member expressions may be added in a future release of this specification.

However, I built a working example on GitHub using Hibernate.

Assuming we have this CalendarEvent entity and the MailingCode DTO object:

@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;
    }
}

You can write the Criteria API code as follows:

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

However, an IN query is a much better choice since the SQL query above is suboptimal.




回答2:


I tried various option but did not work for me. Finally understood, if the second parameter is array for built-in function, it is expanding variables and converting to myVarArgMethod. So what I did, I have written my own custom db functions like below.

If searchKey is single value we can use arrayContains and if searchKey contains multiple values, we can convert list to postrgress array format string using java util function and in postgres function, we can convert that to array by type casting.

Java util methods for converting list to postgress array string and vice versa

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;
    }

Custom postgres function for arrayContains and arrayContainsAny

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;

And we can call above function either in QueryBuilder or @Query annotation like below

In case of QueryBuilder Sample output like below

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

                        };

In case of @Query annotation like below

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

In case of jdbc SQL statement

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


来源:https://stackoverflow.com/questions/24695264/how-to-filter-postgres-array-column-with-the-jpa-criteria-api

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!