Веб-запрос QueryDsl на ключе поля карты

Обзор

Учитывая

  • Spring Данные JPA, Spring Data Rest, QueryDsl
  • a Meetup объект
    • с полем Map<String,String> properties
      • сохранялся в таблице MEETUP_PROPERTY как @ElementCollection
  • a MeetupRepository
    • который расширяет QueryDslPredicateExecutor<Meetup>

Я бы ожидал

Веб-запрос

GET /api/meetup?properties[aKey]=aValue

чтобы возвращать только Meetups с записью свойства, которая имеет указанный ключ и значение: aKey = aValue.

Однако это не работает для меня. Что мне не хватает?

Пробовал

Простые поля

Простые поля работают, например, имя и описание:

GET /api/meetup?name=whatever

Поля коллекции работают, как и участники:

GET /api/meetup?participants.name=whatever

Но не это поле Карты.

Настроить привязки QueryDsl

Я пробовал настроить привязку, имея репозиторий

extend QuerydslBinderCustomizer<QMeetup>

и переопределяя

customize(QuerydslBindings bindings, QMeetup meetup)

но при использовании метода customize() код привязки внутри лямбда не является.

EDIT: узнал, что, поскольку QuerydslBindings средство оценки параметра запроса не позволяет ему сопоставляться с картой pathSpecs, которую она внутренне держит, - которая имеет свои пользовательские привязки в ней.

Некоторые особенности

Поле Meetup.properties

@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "MEETUP_PROPERTY", joinColumns = @JoinColumn(name = "MEETUP_ID"))
@MapKeyColumn(name = "KEY")
@Column(name = "VALUE", length = 2048)
private Map<String, String> properties = new HashMap<>();

настраиваемая привязка querydsl

EDIT: См. выше; оказывается, это ничего не делало для моего кода.

public interface MeetupRepository extends PagingAndSortingRepository<Meetup, Long>,
                                          QueryDslPredicateExecutor<Meetup>,
                                          QuerydslBinderCustomizer<QMeetup> {

    @Override
    default void customize(QuerydslBindings bindings, QMeetup meetup) {
        bindings.bind(meetup.properties).first((path, value) -> {
            BooleanBuilder builder = new BooleanBuilder();
            for (String key : value.keySet()) {
                builder.and(path.containsKey(key).and(path.get(key).eq(value.get(key))));
            }
            return builder;
        });
}

Дополнительные выводы

  • QuerydslPredicateBuilder.getPredicate() просит QuerydslBindings.getPropertyPath() попробовать два способа вернуть путь, чтобы он мог использовать предикат, который может использовать QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess().
    • 1 - посмотреть в пользовательских привязках. Я не вижу способа выразить запрос карты там.
    • 2 по умолчанию - Spring bean. Там же проблема с выражением. Как вы выражаете карту? Таким образом, невозможно получить QuerydslPredicateBuilder.getPredicate() для автоматического создания предиката. Fine - я могу сделать это вручную, если я могу подключиться к QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()

Как я могу переопределить этот класс или заменить bean? Он был создан и возвращен как bean в объявлении RepositoryRestMvcConfiguration.repoRequestArgumentResolver() bean.

  1. я может переопределить bean, объявив мой собственный repoRequestArgumentResolver bean, но он не будет использоваться.
    • Он переопределяется RepositoryRestMvcConfiguration s. Я не могу заставить его, установив его @Primary или @Ordered(HIGHEST_PRECEDENCE).
    • Я могу принудительно выполнить его с помощью явного сканирования компонентов RepositoryRestMvcConfiguration.class, но это также испортит Spring автоконфигурацию загрузки, поскольку это вызывает RepositoryRestMvcConfiguration's bean декларации, подлежащие обработке перед выполнением любой автоматической настройки. Среди прочего, это приводит к ответам, которые Джексону сериализуют нежелательно.

Вопрос

Хорошо - похоже, поддержка, которую я ожидал, просто не существует.

Итак, вопрос становится следующим: КАК правильно ли переопределить repoRequestArgumentResolver bean?

BTW - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver неловко не является общедоступным.:/

Ответы

Ответ 1

Замените Bean

Внедрение ApplicationContextAware

Вот как я заменил bean в контексте приложения.

Он чувствует себя немного взломанным. Мне бы хотелось услышать лучший способ сделать это.

@Configuration
public class CustomQuerydslHandlerMethodArgumentResolverConfig implements ApplicationContextAware {

    /**
     * This class is originally the class that instantiated QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver and placed it into the Spring Application Context
     * as a {@link RootResourceInformationHandlerMethodArgumentResolver} by the name of 'repoRequestArgumentResolver'.<br/>
     * By injecting this bean, we can let {@link #meetupApiRepoRequestArgumentResolver} delegate as much as possible to the original code in that bean.
     */
    private final RepositoryRestMvcConfiguration repositoryRestMvcConfiguration;

    @Autowired
    public CustomQuerydslHandlerMethodArgumentResolverConfig(RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {
        this.repositoryRestMvcConfiguration = repositoryRestMvcConfiguration;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((GenericApplicationContext) applicationContext).getBeanFactory();
        beanFactory.destroySingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME);
        beanFactory.registerSingleton(REPO_REQUEST_ARGUMENT_RESOLVER_BEAN_NAME,
                                      meetupApiRepoRequestArgumentResolver(applicationContext, repositoryRestMvcConfiguration));
    }

    /**
     * This code is mostly copied from {@link RepositoryRestMvcConfiguration#repoRequestArgumentResolver()}, except the if clause checking if the QueryDsl library is
     * present has been removed, since we're counting on it anyway.<br/>
     * That means that if that code changes in the future, we're going to need to alter this code... :/
     */
    @Bean
    public RootResourceInformationHandlerMethodArgumentResolver meetupApiRepoRequestArgumentResolver(ApplicationContext applicationContext,
                                                                                                     RepositoryRestMvcConfiguration repositoryRestMvcConfiguration) {
        QuerydslBindingsFactory factory = applicationContext.getBean(QuerydslBindingsFactory.class);
        QuerydslPredicateBuilder predicateBuilder = new QuerydslPredicateBuilder(repositoryRestMvcConfiguration.defaultConversionService(),
                                                                                 factory.getEntityPathResolver());

        return new CustomQuerydslHandlerMethodArgumentResolver(repositoryRestMvcConfiguration.repositories(),
                                                               repositoryRestMvcConfiguration.repositoryInvokerFactory(repositoryRestMvcConfiguration.defaultConversionService()),
                                                               repositoryRestMvcConfiguration.resourceMetadataHandlerMethodArgumentResolver(),
                                                               predicateBuilder, factory);
    }
}

Создайте предикат для поиска карт из http params

Расширение RootResourceInformationHandlerMethodArgumentResolver

И это фрагменты кода, которые создают мой собственный предикат Map-search на основе параметров http-запроса. Опять же - хотелось бы узнать лучший способ.

Метод postProcess вызывает:

        predicate = addCustomMapPredicates(parameterMap, predicate, domainType).getValue();

просто, прежде чем ссылка predicate передается в конструктор QuerydslRepositoryInvokerAdapter и возвращается.

Вот что addCustomMapPredicates метод:

    private BooleanBuilder addCustomMapPredicates(MultiValueMap<String, String> parameters, Predicate predicate, Class<?> domainType) {
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        parameters.keySet()
                  .stream()
                  .filter(s -> s.contains("[") && matches(s) && s.endsWith("]"))
                  .collect(Collectors.toList())
                  .forEach(paramKey -> {
                      String property = paramKey.substring(0, paramKey.indexOf("["));
                      if (ReflectionUtils.findField(domainType, property) == null) {
                          LOGGER.warn("Skipping predicate matching on [%s]. It is not a known field on domainType %s", property, domainType.getName());
                          return;
                      }
                      String key = paramKey.substring(paramKey.indexOf("[") + 1, paramKey.indexOf("]"));
                      parameters.get(paramKey).forEach(value -> {
                          if (!StringUtils.hasLength(value)) {
                              booleanBuilder.or(matchesProperty(key, null));
                          } else {
                              booleanBuilder.or(matchesProperty(key, value));
                          }
                      });
                  });
        return booleanBuilder.and(predicate);
    }

    static boolean matches(String key) {
        return PATTERN.matcher(key).matches();
    }

И шаблон:

    /**
     * disallow a . or ] from preceding a [
     */
    private static final Pattern PATTERN = Pattern.compile(".*[^.]\\[.*[^\\[]");

Ответ 2

Я провел несколько дней, изучая, как это сделать. В конце концов, я просто добавил вручную предикат. Это решение кажется простым и элегантным.

Таким образом, вы получаете доступ к карте через

GET /api/meetup?properties.aKey=aValue

На контроллере я ввел параметры запроса и предикат.

public List<Meetup> getMeetupList(@QuerydslPredicate(root = Meetup.class) Predicate predicate,
                                                @RequestParam Map<String, String> allRequestParams,
                                                Pageable page) {
    Predicate builder = createPredicateQuery(predicate, allRequestParams);
    return meetupRepo.findAll(builder, page);
}

Затем я просто проанализировал параметры запроса и добавил

private static final String PREFIX = "properties.";

private BooleanBuilder createPredicateQuery(Predicate predicate, Map<String, String> allRequestParams) {
    BooleanBuilder builder = new BooleanBuilder();
    builder.and(predicate);
    allRequestParams.entrySet().stream()
            .filter(e -> e.getKey().startsWith(PREFIX))
            .forEach(e -> {
                var key = e.getKey().substring(PREFIX.length());
                builder.and(QMeetup.meetup.properties.contains(key, e.getValue()));
            });
    return builder;
}