QueryDsl对Map字段

时间:2017-08-31 20:10:27

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

概述

鉴于

  • Spring Data JPA,Spring Data Rest,QueryDsl
  • 一个Meetup实体
    • 带有Map<String,String> properties字段
      • 作为MEETUP_PROPERTY
      • 保存在@ElementCollection表格中
  • a MeetupRepository
    • 扩展了QueryDslPredicateExecutor<Meetup>

我期待

的网络查询
GET /api/meetup?properties[aKey]=aValue

仅返回具有指定键和值的属性条目的Meetup: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()尝试两种方式来返回路径,以便它可以生成QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()可以使用的谓词。
    • 1是查看自定义绑定。我没有看到任何表达地图查询的方法
    • 2默认为Spring的bean路径。那里的表达问题相同。你怎么表达地图? 所以看起来不可能让QuerydslPredicateBuilder.getPredicate()自动创建一个谓词。 很好 - 我可以手动完成,如果我可以挂钩QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver.postProcess()
  2. 我该如何覆盖该类,或者替换bean?它被实例化并作为bean在RepositoryRestMvcConfiguration.repoRequestArgumentResolver() bean声明中返回。

    1. 可以通过声明我自己的repoRequestArgumentResolver bean来覆盖该bean,但它并没有被使用。
      • 它被RepositoryRestMvcConfiguration覆盖。我可以通过设置@Primary@Ordered(HIGHEST_PRECEDENCE) 强制它。
      • 可以通过显式组件扫描RepositoryRestMvcConfiguration.class强制它,但这也会混淆Spring Boot的自动配置,因为它会导致 要处理的RepositoryRestMvcConfiguration's bean声明 在任何自动配置运行之前。除其他外,这导致杰克逊以不受欢迎的方式序列化的回应。
    2. 问题

      好吧 - 看起来像我预期的支持就不存在了。

      所以问题变成了: HOW 我是否正确覆盖了repoRequestArgumentResolver bean?

      BTW - QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver笨拙地非公开。 :/

2 个答案:

答案 0 :(得分:3)

替换Bean

实施ApplicationContextAware

这就是我在应用程序上下文中替换bean的方法。

感觉有点hacky。我很想听到更好的方法来做到这一点。

@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

创建地图搜索谓词

Extend RootResourceInformationHandlerMethodArgumentResolver

这些是基于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(".*[^.]\\[.*[^\\[]");

答案 1 :(得分:1)

我花了几天时间研究如何做到这一点。最后,我只是将手动添加到了谓词中。该解决方案感觉简单而优雅。

因此您可以通过以下方式访问地图

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