#java #spring #jpa
#java #весна #jpa
Вопрос:
Я пытаюсь обобщить процесс фильтрации, чтобы иметь возможность применять фильтры к любому из моих объектов.
Используя некоторый онлайн-код, я создаю это:
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);
}
}
Вот пример URL с фильтрацией:
http://localhost:8080/api/type/284c5f72-559d-4e62-a316-6bd23259c213/slot/filter?filter=name:>Samediamp;filter=id::773f7313-0f37-4465-a707-7295c2bb87a3§UUID
Как вы можете видеть, у меня есть два случая: один только с полем, операцией и значением name:>Samedi
, а второй случай, когда я добавляю тип с символом-разделителем: id::773f7313-0f37-4465-a707-7295c2bb87a3§UUID
Просто для уточнения, другой класс вызывает мою функцию сборки следующим образом:
return (Specification<T>) (root, query, cb) -> op.build(cb, root, key, value);
Например, с ключом как 'id'
и значением как '773f7313-0f37-4465-a707-7295c2bb87a3§UUID'
На самом деле в моем коде я использую builder.equal
функцию для создания предикатов для моего пользовательского типа (здесь UUID ), а не для операции, определенной в начале моего перечисления, но теперь я хочу добавить поддержку для LocalTime
типа и использовать правильную операцию, чтобы иметь возможность использовать a greaterThan
или меньше.
if (type.equals("UUID")) {
return builder.equal(entity.get(key), UUID.fromString(m.group(1)));
} else {
throw new WrongFilterException("Filter operation not found");
}
Для этого:
if (type.equals("UUID")) {
return operation.predicate(builder, entity.get(key), UUID.fromString(m.group(1)));
} else {
throw new WrongFilterException("Filter operation not found");
}
Но тогда у меня возникает эта ошибка:
Required type: String Provided: UUID
Очевидно, что эта ошибка возникла из моего 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:
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, ?> amp; JpaSpecificationExecutor<E>> {
private final R repository;
private Specification<E> filters;
private Sort sort = Sort.unsorted();
public static <E, R extends PagingAndSortingRepository<E, ?> amp; 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;
}
}
И как я вызываю это в своем контроллере:
FindAllBuilder.usingRepository(this.repository).filterBy(filters).findAll(pageable).map(MyMapper.INSTANCE::toDTO);
С фильтром в виде массива строк, который выглядит следующим образом: name:>test
Ужасный способ сделать это — преобразовать мою функцию buildChild во что-то вроде этого:
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());
}
}
Но мне пришлось бы реализовать для каждого типа данных каждый тип операции, это будет много, если.
Ответ №1:
Я пытаюсь получить то, чего вы пытаетесь достичь, но с первого взгляда, если ваша логика в порядке, но функция ожидает строку, и вы передаете UUID, поэтому вместо того, чтобы приводить его к объекту, почему бы не заменить :
UUID.fromString(m.group(1))
с
m.group(1)
таким образом, вы передаете то же значение, но со строковым типом, и если вы используете
UUID.fromString()
чтобы убедиться, что входящая строка находится в формате UUID, вы можете разделить этот шаг проверки.
дайте мне знать, если это сработает с вами.
Комментарии:
1. Да, моя функция предиката в FunctionalInterface ожидает строку, но это проблема, если я приведу свой UUID к string, у меня будет ошибка, которая выглядит так: невозможно сравнить UUID со строкой. Я хочу изменить эту строку на object, чтобы иметь возможность передавать данные любого типа, но это приводит к тому, что мой CriteriaBuilder::method выдает ошибки