简体   繁体   English

在 spring 引导中生成过滤

[英]Generify filtering in spring boot

I'm trying to generify the filtering process to be able to apply filters on any of my entities.我正在尝试生成过滤过程,以便能够在我的任何实体上应用过滤器。

Using some code online I build this:使用一些在线代码我构建了这个:

public enum FilterOperation {
    // @formatter:off
    LESS_EQUAL_THAN("<=", CriteriaBuilder::lessThanOrEqualTo),
    GREATER_EQUAL_THAN(">=", CriteriaBuilder::greaterThanOrEqualTo),
    CONTAINS(":>", CriteriaBuilder::like),
    GREATER_THAN(">", CriteriaBuilder::greaterThan),
    LESS_THAN("<", CriteriaBuilder::lessThan),
    EQUALS("::", CriteriaBuilder::equal);
    // @formatter:on

    private final String operationName;
    private final FilterPredicateFunction operation;

    private static Pattern separator = Pattern.compile("([\\S\\s]+)§([\\S\\s]+)");

    FilterOperation(String operationName, FilterPredicateFunction operation) {
        this.operationName = operationName;
        this.operation = operation;
    }

    public String getOperationName() {
        return operationName;
    }

    public Predicate build(CriteriaBuilder builder, Root<?> entity, String key, String value) {
        // Split value by character §
        Matcher m = separator.matcher(value);
        if (m.matches()) {
            String type = m.group(2);
            if (type.equals("UUID")) {
                return builder.equal(entity.get(key), UUID.fromString(m.group(1)));
            } else {
                throw new WrongFilterException("Filter operation not found");
            }
        } else {
            // No § found we expect a string search
            return operation.predicate(builder, entity.get(key), value.toString());
        }
    }

    static FilterOperation parse(String str) {
        for (FilterOperation filter : FilterOperation.values()) {
            if (str.equals(filter.getOperationName())) {
                return filter;
            }
        }

        throw new WrongFilterException(String.format("Filter operation not found", str));
    }

    @FunctionalInterface
    interface FilterPredicateFunction {
        Predicate predicate(CriteriaBuilder builder, Path<String> key, String value);
    }
}

Here is an example of URL with filtering:这是一个带有过滤的 URL 示例:

http://localhost:8080/api/type/284c5f72-559d-4e62-a316-6bd23259c213/slot/filter?filter=name:>Samedi&filter=id::773f7313-0f37-4465-a707-7295c2bb87a3§UUID

As you can see I have two cases, one with only a field, an operation, and the value name:>Samedi and a second case where I add the type with a separator character: id::773f7313-0f37-4465-a707-7295c2bb87a3§UUID如您所见,我有两种情况,一种只有一个字段、一个操作和值name:>Samedi ,第二种情况是我添加了带有分隔符的类型: id::773f7313-0f37-4465-a707-7295c2bb87a3§UUID

Just to precise, another class call my build function like this:准确地说,另一个 class 像这样调用我的构建 function:

return (Specification<T>) (root, query, cb) -> op.build(cb, root, key, value);

With for example key as 'id' and value as '773f7313-0f37-4465-a707-7295c2bb87a3§UUID'例如,键为'id' ,值为'773f7313-0f37-4465-a707-7295c2bb87a3§UUID'

In my code actually, I use the builder.equal function to create Predicates for my custom type (here UUID) and not the operation defined at the beginning of my enum, but now I want to add the support for the LocalTime type and use the right operation to be able to use a greaterThan or less than.实际上,在我的代码中,我使用builder.equal function 为我的自定义类型(此处为 UUID)创建谓词,而不是在枚举开头定义的操作,但现在我想添加对LocalTime类型的支持并使用正确的操作要能够使用greaterThan或小于。

 if (type.equals("UUID")) {
     return builder.equal(entity.get(key), UUID.fromString(m.group(1)));
 } else {
     throw new WrongFilterException("Filter operation not found");
 }

To this:对此:

if (type.equals("UUID")) {
  return operation.predicate(builder, entity.get(key), UUID.fromString(m.group(1)));
} else {
  throw new WrongFilterException("Filter operation not found");
}

But then I have this error:但后来我有这个错误:

Required type: String Provided: UUID

Obviously, that error came from my FilterPredicateFunction that can only take a String, I try to change it to object but then all my CriteriaBuilder::methodName get this error: Cannot resolve method 'greaterThanOrEqualTo'显然,该错误来自我的FilterPredicateFunction只能接受一个字符串,我尝试将其更改为 object 但随后我的所有CriteriaBuilder::methodName都收到此错误: Cannot resolve method 'greaterThanOrEqualTo'

What can I do to use any operation on any type of data?我可以做些什么来对任何类型的数据使用任何操作? Any idea on how I can achieve this?关于如何实现这一目标的任何想法?

EDIT: I add all other class that I use to better understand the process:编辑:我添加了所有其他 class 以更好地理解该过程:

public class EntitySpecificationBuilder<T> {

    @SuppressWarnings({"rawtypes", "unchecked"})
    public static <T> Optional<Specification<T>> parse(List<String> filters) {
        if (filters == null || filters.isEmpty()) {
            return Optional.empty();
        }

        List<Specification> criterias = mapSpecifications(filters);
        if (criterias.isEmpty()) {
            return Optional.empty();
        }

        Specification<T> root = Specification.where(criterias.get(0));
        for (int index = 1; index < criterias.size(); index++) {
            root = Specification.where(root).and(criterias.get(index));
        }
        return Optional.of(root);
    }

    @SuppressWarnings("rawtypes")
    private static <T> List<Specification> mapSpecifications(List<String> filters) {
        return filters.stream().map(str -> {
            for (FilterOperation op : FilterOperation.values()) {
                int index = str.indexOf(op.getOperationName());
                if (index > 0) {
                    String key = str.substring(0, index);
                    String value = str.substring(index + op.getOperationName().length());

                        return (Specification<T>) (root, query, cb) -> op.build(cb, root, key, value);

                }
            }

            return null;
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }
}

The builder:建设者:

public class FindAllBuilder<E, R extends PagingAndSortingRepository<E, ?> & JpaSpecificationExecutor<E>> {

    private final R repository;

    private Specification<E> filters;

    private Sort sort = Sort.unsorted();

    public static <E, R extends PagingAndSortingRepository<E, ?> & JpaSpecificationExecutor<E>> FindAllBuilder<E, R> usingRepository(
            R repository) {
        return new FindAllBuilder<>(repository);
    }

    private FindAllBuilder(R repository) {
        this.repository = repository;
    }

    public Page<E> findAll(Pageable pageable) {
        return repository.findAll(filters, pageable);
    }

    public FindAllBuilder<E, R> filterBy(List<String> listFilters) {
        Optional<Specification<E>> opFilters = EntitySpecificationBuilder.parse(listFilters);
        if (opFilters.isPresent()) {
            if (filters == null) {
                filters = Specification.where(opFilters.get());
            } else {
                filters = filters.and(opFilters.get());
            }
        }

        return this;
    }

    public FindAllBuilder<E, R> sortBy(String orderBy, String orderDir) {
        if (!orderBy.isEmpty()) {
            sort = Sort.by(Direction.fromOptionalString(orderDir).orElse(Direction.ASC), orderBy);
        }

        return this;
    }
}

And how I call it in my controller:以及我在 controller 中如何称呼它:

FindAllBuilder.usingRepository(this.repository).filterBy(filters).findAll(pageable).map(MyMapper.INSTANCE::toDTO);

With filter as an array of string that looks like this: name:>test使用 filter 作为字符串数组,如下所示: name:>test

The awful way to do this would to transform my buildChild function to something like this:这样做的糟糕方法是将我的 buildChild function 转换为以下内容:

public Predicate buildChild(CriteriaBuilder builder, Root<?> entity, String pKey, String cKey, String value) {
    Matcher m = separator.matcher(value);
    if (m.matches()) {
        String type = m.group(2);
        if (type.equals("UUID")) {
            if(operationName.equals(FilterOperation.EQUALS.getOperationName())) {
                return builder.equal(entity.get(pKey).get(cKey), UUID.fromString(m.group(1)));
            } else if (operationName.equals(FilterOperation.CONTAINS.getOperationName())) {
                return builder.like(...);
            } etc...
            
        } else if (type.equals("LocalTime")) {
            ....
        }
        
        else {
            throw new WrongFilterException("Filter operation not found");
        }
    } else {
        // No § found we expect a string search
        return operation.predicate(builder, entity.get(pKey).get(cKey), value.toString());
    }
}

But I would have to implement for each type of data each type of operation, this will be a lot of ifs.但是我必须为每种类型的数据执行每种类型的操作,这将是很多 ifs。

I'm trying to get what you are trying to achieve, but from first look, if your logic is fine but the function waits for String and you pass UUID , so Instead of casting it to Object why not to replace:我试图得到你想要达到的目标,但从第一眼看,如果你的逻辑很好,但 function 等待String并且你通过UUID ,所以不要将它转换为Object为什么不替换:

UUID.fromString(m.group(1))

with

m.group(1)

so you pass the same value but with String type, and if you are using所以你传递相同的值但使用String类型,如果你正在使用

UUID.fromString()
to make sure the incoming string is in UUID format you can separate this validation step. 要确保传入的字符串是 UUID 格式,您可以分离此验证步骤。

let me know if it works with you.让我知道它是否适合你。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM