【问题标题】:QueryDsl web query on the key of a Map fieldQueryDsl 对 Map 字段键的 Web 查询
【发布时间】:2018-02-09 21:00:21
【问题描述】:

概述

给定

  • Spring Data JPA、Spring Data Rest、QueryDsl
  • Meetup 实体
    • 带有Map<String,String> properties 字段
      • @ElementCollection 的形式坚持在MEETUP_PROPERTY 表中
  • MeetupRepository
    • 扩展QueryDslPredicateExecutor<Meetup>

我希望

网络查询

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

只返回具有指定键和值的属性条目的聚会:aKey=aValue。

但是,这对我不起作用。 我错过了什么?

试过

简单字段

使用简单的字段,例如名称和描述:

GET /api/meetup?name=whatever

收集字段像参与者一样工作:

GET /api/meetup?participants.name=whatever

但不是这个 Map 字段。

自定义 QueryDsl 绑定

我尝试通过拥有存储库来自定义绑定

extend QuerydslBinderCustomizer<QMeetup>

并覆盖

customize(QuerydslBindings bindings, QMeetup meetup)

方法,但是当customize() 方法被命中时,lambda 内部的绑定代码没有。

编辑:了解到这是因为 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 绑定

编辑:见上文;事实证明,这对我的代码没有任何作用。

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

其他发现

  1. QuerydslPredicateBuilder.getPredicate() 要求 QuerydslBindings.getPropertyPath() 尝试 2 种方法来返回路径,以便它可以创建一个 QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess() 可以使用的谓词。
    • 1 是查看自定义绑定。我没有看到任何表达地图查询的方法
    • 2 默认为 Spring 的 bean 路径。那里有同样的表达问题。你如何表达一张地图? 所以看起来不可能让QuerydslPredicateBuilder.getPredicate() 自动创建谓词。 很好 - 我可以手动完成,如果我可以连接到 QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()

如何覆盖该类或替换 bean?它在 RepositoryRestMvcConfiguration.repoRequestArgumentResolver() bean 声明中被实例化并作为 bean 返回。

  1. 可以通过声明我自己的 repoRequestArgumentResolver bean 来覆盖该 bean,但它没有被使用。
    • 它被RepositoryRestMvcConfigurations 覆盖。我无法通过设置@Primary@Ordered(HIGHEST_PRECEDENCE)强制它。
    • 可以通过显式组件扫描 RepositoryRestMvcConfiguration.class 来强制它,但这也会扰乱 Spring Boot 的自动配置,因为它会导致 RepositoryRestMvcConfiguration's 要处理的 bean 声明 在任何自动配置运行之前。除其他外,这会导致 Jackson 以不需要的方式序列化响应。

问题

嗯 - 看起来我期望的支持不存在。

所以问题变成了: 如何正确覆盖repoRequestArgumentResolver bean?

顺便说一句 - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver 尴尬地不公开。 :/

【问题讨论】:

    标签: java spring spring-data-jpa spring-data-rest querydsl


    【解决方案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 参数创建地图搜索谓词

    扩展 RootResourceInformationHandlerMethodArgumentResolver

    这些是基于 http 查询参数创建我自己的地图搜索谓词的代码 sn-ps。 再次 - 很想知道更好的方法。

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

      然后我只是简单地解析了查询参数并添加了 contains

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

      【讨论】:

      • 我喜欢你的选择,包含作品。但是我仍然无法通过结果来增加谓词的复杂性。例如:if(key.startsWith("min")){builder.and(QServiceLimit.serviceLimit.attribute.get(key).gt(Double.parseDouble(e.getValue())));}
      • 您能进一步扩展吗?您的示例的请求参数是什么?地图上的复杂查询(如 gt、lt、between 等)对我有用……您的查询参数可能包含“您要执行的功能”,例如。 properties.age:min=45&amp;properties.age:max=55我已经使用这种查询日期范围from:prop.date=10-11-2019&amp;to:prop.date=11-11-2019
      猜你喜欢
      • 2014-12-04
      • 1970-01-01
      • 1970-01-01
      • 2015-10-21
      • 2016-08-07
      • 1970-01-01
      • 2016-03-28
      • 2012-04-25
      • 2023-04-05
      相关资源
      最近更新 更多