Kollidiert?
In diesem Artikel wird erläutert, wie Sie in Spring + JPA / Hibernate Abfragen für eine Tabelle mit einer sich ändernden Liste von Kriterien durchführen können, ohne zusätzliche Bibliotheken anzuhängen.
Es gibt nur zwei Hauptfragen:
- So stellen Sie eine SQL-Abfrage dynamisch zusammen
- Wie man die Bedingungen für die Bildung dieser Anfrage erfüllt
Für das Zusammenstellen von JPA-Anforderungen ab 2.0 (
Spezifikation - Gesamtabfrageeinschränkungen, enthält Prädikatobjekte als WHERE, HAVING-Bedingungen. Prädikate sind endgültige Ausdrücke, die wahr oder falsch sein können.
Eine einzelne Bedingung besteht aus einem Feld, einem Vergleichsoperator und einem zu vergleichenden Wert. Bedingungen können auch verschachtelt werden. Lassen Sie uns den Zustand mit der SearchCriteria-Klasse vollständig beschreiben:
public class SearchCriteria{
//
String key;
// (, .)
SearchOperator operator;
//
String value;
//
private JoinType joinType;
//
private List<SearchCriteria> criteria;
}
Beschreiben wir nun den Builder selbst. Er wird in der Lage sein, eine Spezifikation basierend auf der eingereichten Liste von Bedingungen zu erstellen und mehrere Spezifikationen auf eine bestimmte Weise zu kombinieren:
/**
*
*/
public class JpaSpecificationsBuilder<T> {
// join-
private Map<String,Join<Object, Object>> joinMap = new HashMap<>();
//
private Map<SearchOperation, PredicateBuilder> predicateBuilders = Stream.of(
new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.EQ,new EqPredicateBuilder()),
new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.MORE,new MorePredicateBuilder()),
new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.MOREQ,new MoreqPredicateBuilder()),
new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.LESS,new LessPredicateBuilder()),
new AbstractMap.SimpleEntry<SearchOperation,PredicateBuilder>(SearchOperation.LESSEQ,new LesseqPredicateBuilder())
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
/**
*
*/
public Specification<T> buildSpecification(SearchCriteria criterion){
this.joinMap.clear();
return (root, query, cb) -> buildPredicate(root,cb,criterion);
}
/**
*
*/
public Specification<T> mergeSpecifications(List<Specification> specifications, JoinType joinType) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
specifications.forEach(specification -> predicates.add(specification.toPredicate(root, query, cb)));
if(joinType.equals(JoinType.AND)){
return cb.and(predicates.toArray(new Predicate[0]));
}
else{
return cb.or(predicates.toArray(new Predicate[0]));
}
};
}
}
Um große Vergleichsoperationen nicht zu verhindern, implementieren wir Kartenoperatoren der Form <Operation, Operator>. Der Operator muss in der Lage sein, ein einzelnes Prädikat zu erstellen. Ich werde ein Beispiel für die Operation ">" geben, der Rest ist analog geschrieben:
public class EqPredicateBuilder implements PredicateBuilder {
@Override
public SearchOperation getManagedOperation() {
return SearchOperation.EQ;
}
@Override
public Predicate getPredicate(CriteriaBuilder cb, Path path, SearchCriteria criteria) {
if(criteria.getValue() == null){
return cb.isNull(path);
}
if(LocalDateTime.class.equals(path.getJavaType())){
return cb.equal(path,LocalDateTime.parse(criteria.getValue()));
}
else {
return cb.equal(path, criteria.getValue());
}
}
}
Jetzt muss noch die rekursive Analyse unserer SearchCriteria-Struktur implementiert werden. Beachten Sie, dass die buildPath-Methode, die nach Root - dem Bereich des Objekts T - den Pfad zu dem Feld findet, auf das SearchCriteria.key verweist:
private Predicate buildPredicate(Root<T> root, CriteriaBuilder cb, SearchCriteria criterion) {
if(criterion.isComplex()){
List<Predicate> predicates = new ArrayList<>();
for (SearchCriteria subCriterion : criterion.getCriteria()) {
// ,
predicates.add(buildPredicate(root,cb,subCriterion));
}
if(JoinType.AND.equals(criterion.getJoinType())){
return cb.and(predicates.toArray(new Predicate[0]));
}
else{
return cb.or(predicates.toArray(new Predicate[0]));
}
}
return predicateBuilders.get(criterion.getOperation()).getPredicate(cb,buildPath(root, criterion.getKey()),criterion);
}
private Path buildPath(Root<T> root, String key) {
if (!key.contains(".")) {
return root.get(key);
} else {
String[] path = key.split("\\.");
String subPath = path[0];
if(joinMap.get(subPath) == null){
joinMap.put(subPath,root.join(subPath));
}
for (int i = 1; i < path.length-1; i++) {
subPath = Stream.of(path).limit(i+1).collect(Collectors.joining("."));
if(joinMap.get(subPath) == null){
String prevPath = Stream.of(path).limit(i).collect(Collectors.joining("."));
joinMap.put(subPath,joinMap.get(prevPath).join(path[i]));
}
}
return joinMap.get(subpath).get(path[path.length - 1]);
}
}
Schreiben wir einen Testfall für unseren Builder:
// Entity
@Entity
public class ExampleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
public int value;
public ExampleEntity(int value){
this.value = value;
}
}
...
//
@Repository
public interface ExampleEntityRepository extends JpaRepository<ExampleEntity,Long>, JpaSpecificationExecutor<ExampleEntity> {
}
...
//
/*
*/
public class JpaSpecificationsTest {
@Autowired
private ExampleEntityRepository exampleEntityRepository;
@Test
public void getWhereMoreAndLess(){
exampleEntityRepository.save(new ExampleEntity(3));
exampleEntityRepository.save(new ExampleEntity(5));
exampleEntityRepository.save(new ExampleEntity(0));
SearchCriteria criterion = new SearchCriteria(
null,null,null,
Arrays.asList(
new SearchCriteria("value",SearchOperation.MORE,"0",null,null),
new SearchCriteria("value",SearchOperation.LESS,"5",null,null)
),
JoinType.AND
);
assertEquals(1,exampleEntityRepository.findAll(specificationsBuilder.buildSpecification(criterion)).size());
}
}
Insgesamt haben wir unserer Anwendung beigebracht, einen booleschen Ausdruck mithilfe der Criteria.API zu analysieren. Die Anzahl der Operationen in der aktuellen Implementierung ist begrenzt, aber der Leser kann die von ihm benötigten unabhängig implementieren. In der Praxis wurde die Lösung angewendet, aber Benutzer sind nicht daran interessiert (
Der DISCLAIMER- Handler erhebt keinen Anspruch auf vollständige Universalität. Wenn Sie komplizierte JOINs hinzufügen müssen, müssen Sie mit der Implementierung beginnen.
Die implementierte Version mit erweiterten Tests finden Sie in meinem Repository auf Github .
Weitere Informationen zu Criteria.Api finden Sie hier .