Динамический запрос данных в хранилище данных jpa с произвольными предложениями AND
Я использую Spring data jpa repositories
, Получил требование предоставить функцию поиска с разными полями. Ввод полей перед поиском необязателен. У меня есть 5 полей, скажем EmployeeNumber
, Name
, Married
,Profession
а также DateOfBirth
,
Здесь мне нужно запросить только с заданными значениями пользователя и другие поля должны быть проигнорированы.Ex,
Input : EmployeeNumber: ,Name:St,Married: ,Professsion:IT,DateOfBirth:
Query : Select * from Employee e where Name like 'St%' and Profession like 'IT%';
Input : EmployeeNumber:10,Name: ,Married: ,Professsion:IT,DateOfBirth:
Query : Select * from Employee e where EmployeeNumber like '10%' and Profession like 'IT%';
Итак, здесь мы рассматриваем введенные значения и запрашиваем. В этом случае данные Spring имеют ограничение, как упомянуто в этом посте (не масштабируется, и все возможные запросы должны быть написаны). Я использую Querydsl
, но все же проблема существует как null
поля следует игнорировать, и почти все возможные запросы должны быть разработаны. В этом case 31 queries
, Что делать, если поля поиска 6,7,8...
??
Каков наилучший подход для реализации опции поиска с необязательными полями?
4 ответа
Обратите внимание, что могут быть внесены изменения для использования новой основной версии QueryDSL (4.x) и querydsl-jpa.
В одном из наших проектов мы использовали QueryDSL
с QueryDslPredicateExecutor<T>
,
public Predicate createPredicate(DataEntity dataEntity) {
QDataEntity qDataEntity = QDataEntity.dataEntity;
BooleanBuilder booleanBuilder = new BooleanBuilder();
if (!StringUtils.isEmpty(dataEntity.getCnsiConsumerNo())) {
booleanBuilder
.or(qDataEntity.cnsiConsumerNo.contains(dataEntity.getCnsiConsumerNo()));
}
if (!StringUtils.isEmpty(dataEntity.getCnsiMeterNo())) {
booleanBuilder.or(qDataEntity.cnsiMeterNo.contains(dataEntity.getCnsiMeterNo()));
}
return booleanBuilder.getValue();
}
И мы могли бы использовать это в репозиториях:
@Repository
public interface DataEntityRepository
extends DaoRepository<DataEntity, Long> {
куда DaoRepository
является
@NoRepositoryBean
public interface DaoRepository<T, K extends Serializable>
extends JpaRepository<T, K>,
QueryDslPredicateExecutor<T> {
}
Потому что тогда вы можете использовать методы предикатов репозитория.
Iterable<DataEntity> results = dataEntityRepository.findAll(dataEntityPredicateCreator.createPredicate(dataEntity));
Получить QClasses
, вам нужно указать плагин QueryDSL APT Maven в вашем pom.xml.
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>maven-apt-plugin</artifactId>
<version>1.0.4</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources</outputDirectory>
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
Зависимости
<!-- querydsl -->
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
<version>${querydsl.version}</version>
</dependency>
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
</dependency>
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>
Или для Gradle:
sourceSets {
generated
}
sourceSets.generated.java.srcDirs = ['src/main/generated']
configurations {
querydslapt
}
dependencies {
// other deps ....
compile "com.mysema.querydsl:querydsl-jpa:3.6.3"
compile "com.mysema.querydsl:querydsl-apt:3.6.3:jpa"
}
task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
source = sourceSets.main.java
classpath = configurations.compile + configurations.querydslapt
options.compilerArgs = [
"-proc:only",
"-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
]
destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}
compileJava {
dependsOn generateQueryDSL
source generateQueryDSL.destinationDir
}
compileGeneratedJava {
dependsOn generateQueryDSL
classpath += sourceSets.main.runtimeClasspath
}
Вы можете использовать спецификации, которые Spring-data предоставляет вам из коробки. и быть в состоянии использовать API критериев для программного создания запросов. Для поддержки спецификаций вы можете расширить интерфейс своего репозитория с помощью интерфейса JpaSpecificationExecutor
public interface CustomerRepository extends SimpleJpaRepository<T, ID>, JpaSpecificationExecutor {
}
Дополнительный интерфейс (JpaSpecificationExecutor) содержит методы, которые позволяют вам выполнять Спецификации различными способами.
Например, метод findAll вернет все сущности, которые соответствуют спецификации:
List<T> findAll(Specification<T> spec);
Интерфейс спецификации выглядит следующим образом:
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder);
}
Итак, каков типичный вариант использования? Спецификации могут быть легко использованы для создания расширяемого набора предикатов поверх сущности, который затем можно комбинировать и использовать с JpaRepository без необходимости объявлять запрос (метод) для каждой необходимой комбинации. Вот пример: Пример 2.15. Спецификации для клиента
public class CustomerSpecs {
public static Specification<Customer> isLongTermCustomer() {
return new Specification<Customer>() {
public Predicate toPredicate(
Root<Customer> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
LocalDate date = new LocalDate().minusYears(2);
return builder.lessThan(root.get('dateField'), date);
}
};
}
public static Specification<Customer> hasSalesOfMoreThan(MontaryAmount value) {
return new Specification<Customer>() {
public Predicate toPredicate(
Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
// build query here
}
};
}
}
Вы изложили некоторые критерии уровня абстракции бизнес-требований и создали исполняемые спецификации. Таким образом, клиент может использовать Спецификацию следующим образом:
List customers = customerRepository.findAll(isLongTermCustomer());
Вы также можете объединить Пример спецификации 2.17. Комбинированные спецификации
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));
Как вы можете видеть, Спецификации предлагают несколько методов склеивания кода для объединения и объединения Спецификаций. Таким образом, расширение уровня доступа к данным - это всего лишь вопрос создания новых реализаций спецификации и их объединения с уже существующими.
И вы можете создать сложные спецификации, вот пример
public class WorkInProgressSpecification {
public static Specification<WorkInProgress> findByCriteria(final SearchCriteria searchCriteria) {
return new Specification<WorkInProgress>() {
@Override
public Predicate toPredicate(
Root<WorkInProgress> root,
CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (searchCriteria.getView() != null && !searchCriteria.getView().isEmpty()) {
predicates.add(cb.equal(root.get("viewType"), searchCriteria.getView()));
}
if (searchCriteria.getFeature() != null && !searchCriteria.getFeature().isEmpty()) {
predicates.add(cb.equal(root.get("title"), searchCriteria.getFeature()));
}
if (searchCriteria.getEpic() != null && !searchCriteria.getEpic().isEmpty()) {
predicates.add(cb.equal(root.get("epic"), searchCriteria.getEpic()));
}
if (searchCriteria.getPerformingGroup() != null && !searchCriteria.getPerformingGroup().isEmpty()) {
predicates.add(cb.equal(root.get("performingGroup"), searchCriteria.getPerformingGroup()));
}
if (searchCriteria.getPlannedStartDate() != null) {
System.out.println("searchCriteria.getPlannedStartDate():" + searchCriteria.getPlannedStartDate());
predicates.add(cb.greaterThanOrEqualTo(root.<Date>get("plndStartDate"), searchCriteria.getPlannedStartDate()));
}
if (searchCriteria.getPlannedCompletionDate() != null) {
predicates.add(cb.lessThanOrEqualTo(root.<Date>get("plndComplDate"), searchCriteria.getPlannedCompletionDate()));
}
if (searchCriteria.getTeam() != null && !searchCriteria.getTeam().isEmpty()) {
predicates.add(cb.equal(root.get("agileTeam"), searchCriteria.getTeam()));
}
return cb.and(predicates.toArray(new Predicate[] {}));
}
};
}
}
Из Spring Data JPA 1.10 есть еще один вариант для этого - Query By Example. Ваш репозиторий должен реализовываться отдельно JpaRepository
также интерфейс QueryByExampleExecutor, где вы получаете такие методы, как:
<S extends T> Iterable<S> findAll(Example<S> example)
Затем вы создаете пример для поиска как:
Employee e = new Employee();
e.setEmployeeNumber(getEmployeeNumberSomewherFrom());
e.setName(getNameSomewhereFrom());
e.setMarried(getMarriedSomewhereFrom());
e.setProfession(getProfessionSomewhereFrom());
e.setDateOfBirth(getDateOfBirthSomewhereFrom());
а потом:
employeeRepository.findAll(Example.of(e));
Если некоторые параметры имеют значение null, они не будут включены в предложение WHERE, поэтому вы получите динамические запросы.
Чтобы уточнить соответствие атрибутов String, взгляните на ExampleMatcher
"s
ExampleMatcher
который делает без учета регистра like
это например:
ExampleMatcher matcher = ExampleMatcher.matching().
withMatcher("profession", ExampleMatcher.GenericPropertyMatcher.of(ExampleMatcher.StringMatcher.CONTAINING).ignoreCase());
Примеры QBE: https://github.com/spring-projects/spring-data-examples/tree/master/jpa/query-by-example
Немного поздно для игры, но ответы здесь слишком сложные ... что, если вы измените поля своей сущности? Что, если вы хотите поддерживать поиск по разным объектам?
Вы можете просто использовать эту библиотеку: https://github.com/turkraft/spring-filter
Это позволит вам запускать такие поисковые запросы, как:
/search?filter= средний (рейтинги) > 4.5 и название бренда в ('audi', 'land rover') и (год > 2018 или км < 50000) и цвет : 'белый' и аварии пуст
В сочетании с Spring's Pageable вы сможете разбивать на страницы
&page=11&size=20