Suchen mit Spring Data JPA Specification

“Die Lösung ist immer einfach, man muss sie nur finden.”

Alexander Solschenizyn

Durch den Einsatz von Spring Data JPA in eigenen Projekten können Datenbankaktionen sehr einfach mit Hilfe von

Repository
Repository Interfaces umgesetzt werden. Alle Zugriffe auf die Entitäten erfolgen über Methoden dieser Schnittstellen.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public interface AncestorRepository extends JpaRepository<Ancestor, Long> {
List <Ancestor> findByLastNameLikeAndBirthBetween(String lastName, LocalDate start, LocalDate end, Pageable pageable);
}
public interface AncestorRepository extends JpaRepository<Ancestor, Long> { List <Ancestor> findByLastNameLikeAndBirthBetween(String lastName, LocalDate start, LocalDate end, Pageable pageable); }
public interface AncestorRepository extends JpaRepository<Ancestor, Long> {
  List <Ancestor> findByLastNameLikeAndBirthBetween(String lastName, LocalDate start, LocalDate end, Pageable pageable);
}

In diesem Beispiel wird eine Schnittstelle

AncestorRepository
AncestorRepository definiert, das von
JpaRespository
JpaRespository erbt und als Entität die Klasse
Ancestor
Ancestor und als ID die Klasse Long nutzt.

Diese Schnittstelle definiert auch eine Methode

findByLastNameLikeAndBirthBetween
findByLastNameLikeAndBirthBetween mit der alle
Ancestor
Ancestor gefunden werden, deren Nachname den Parameter
lastName
lastName gleicht und deren Geburtsdatum zwischen dem Datum
start
start und
end
end liegt. Das Ergebnis wird in einer Liste zurückgegeben, die eine maximale Anzahl von Elementen enthält.

All diese Informationen entnimmt Spring Data JPA der Methodensignatur und generiert im Hintergrund eine entsprechende Implementierung.

Da das

AncestorRepository
AncestorRepository aber nicht direkt die Schnittstelle Repository erweitert, erhält sie auch alle Zugriffsmethoden aller anderen erweiterten Schnittstellen. In diesem Fall von den Schnittstellen
CrudRepository
CrudRepository (
save
save,
saveAll
saveAll,
delete
delete,
deleteAll
deleteAll,
deleteById
deleteById,
deleteAllById
deleteAllById,
findById
findById,
findAll
findAll,
findAllById
findAllById,
existsById
existsById,
count
count),
PagingAndSortingRepository
PagingAndSortingRepository (
findAll
findAll mit
Pageable
Pageable und
Sort
Sort) und
JpaRepository
JpaRepository (
saveAndFlush
saveAndFlush,
deleteInBatch
deleteInBatch,
findAll
findAll mit
Example
Example und
Sort
Sort, …).

Mit diesen vordefinierten Zugriffsmethoden und der Möglichkeit eigene Zugriffsmethoden zu deklarieren ist ein Großteil der üblichen Anwendungen abgedeckt. Einziger Nachteil dieser Methoden ist ihre feste Definition. Für jede Anfrage mit einem etwas anderen Satz von Suchparametern muss eine neue Methode definiert werden.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
List<Ancestor> ancestors = repository.findByLastNameAndFirstNameAndMiddleNameAndTitleLikeAndBirthPlaceLikeAndBirthBetween(String lastName, String firstName, String middleName, String title, String birthPlace, LocalDate start, LocalDate end, Pageable pageable);
List<Ancestor> ancestors = repository.findByLastNameAndFirstNameAndMiddleNameAndTitleLikeAndBirthPlaceLikeAndBirthBetween(String lastName, String firstName, String middleName, String title, String birthPlace, LocalDate start, LocalDate end, Pageable pageable);
List<Ancestor> ancestors = repository.findByLastNameAndFirstNameAndMiddleNameAndTitleLikeAndBirthPlaceLikeAndBirthBetween(String lastName, String firstName, String middleName, String title, String birthPlace, LocalDate start, LocalDate end, Pageable pageable);

In diesem Beispiel wird nach Titel, Vor-, Mittel- und Nachnamen, Geburtsort und Geburtsdatum gesucht. Schon bei diesen wenigen Suchparametern wird klar, dass dieser Ansatz zu einer großen Zahl von schlecht verständlichen Methoden führt.

Ein alternativer Ansatz, um die Suchen dynamisch zu generieren, verwendet die Schnittstelle

Specification
Specification.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}
interface Specification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb); }
interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}

Mit Hilfe dieser Schnittstelle werden die einzelnen Komponenten einer Suche definiert und können über die Methoden

and
and,
or
or und
not
not verknüpft werden. Über den
Root
Root Parameter kann innerhalb der
Specification
Specification auf Attribute der Entität zugegriffen werden und der
CriteriaBuilder
CriteriaBuilder liefert eine lange Liste von Methoden um Vergleiche zu erstellen, Ausdrücke zu verknüpfen und vieles mehr.

Damit das eigene

AncestorRepository
AncestorRepository mit den
Specification
Specification umgehen kann, muss es die Schnittstelle
JpaSpecificationExcutor
JpaSpecificationExcutor erweitern. Damit stehen einige
find
find Methoden zur Verfügung, die einen Parameter vom Typ
Specification
Specification besitzen.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public interface AncestorRepository extends JpaRepository<Ancestor, Long>, JpaSpecificationExecutor<Ancestor> {
@NonNull
static Specification<Ancestor> gender(Gender search) {
return (ancestor, cq, cb) -> cb.equal(ancestor.<Gender>get("gender"), search);
}
@NonNull
static Specification<Ancestor> lastName(String search) {
return (ancestor, cq, cb) -> cb.like(cb.lower(ancestor.get("lastname")), "%" + search.toLowerCase() + "%");
}
}
public interface AncestorRepository extends JpaRepository<Ancestor, Long>, JpaSpecificationExecutor<Ancestor> { @NonNull static Specification<Ancestor> gender(Gender search) { return (ancestor, cq, cb) -> cb.equal(ancestor.<Gender>get("gender"), search); } @NonNull static Specification<Ancestor> lastName(String search) { return (ancestor, cq, cb) -> cb.like(cb.lower(ancestor.get("lastname")), "%" + search.toLowerCase() + "%"); } }
public interface AncestorRepository extends JpaRepository<Ancestor, Long>, JpaSpecificationExecutor<Ancestor> {

  @NonNull
  static Specification<Ancestor> gender(Gender search) {
    return (ancestor, cq, cb) -> cb.equal(ancestor.<Gender>get("gender"), search);
  }

  @NonNull
  static Specification<Ancestor> lastName(String search) {
    return (ancestor, cq, cb) -> cb.like(cb.lower(ancestor.get("lastname")), "%" + search.toLowerCase() + "%");
  }
}

Dieses

AncestorRepository
AncestorRepository erweitert
JpaSpecificationExcutor
JpaSpecificationExcutor und kann deshalb mit
Specification
Specification umgehen. Die beiden Convenience Methoden
gender
gender und
lastName
lastName erzeugen eine
Specification
Specification, die auf dem jeweiligen Eingabeparameter
search
search basiert. Bei
gender
gender wird ein Vergleich mit dem Attribute gender vorgenommen. Bei
lastname
lastname wird geprüft, ob der Parameter im Attribut
lastname
lastname vorkommt. Dabei wird die Groß-/Kleinschreibung nicht beachtet.

Im folgendem Bespiel wird eine Suche nach männlichen Vorfahren ohne den Nachnamen Kaiser durchgeführt. Die erste Variante nutzt keine statischen Importe und ist deshalb schwieriger zu lesen und zu verstehen. Die zweite Variante ist, dank der fluent API, sehr einfach zu verstehen.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
List<Ancestor> ancestors = repository.findAll(
Specification.where(AncestorRepository.gender(Gender.MALE))
.and(Specification.not(AncestorRepository.lastName("Kaiser")))), pageable);
List<Ancestor> ancestors = repository.findAll(where(gender(Gender.MALE)).and(not(lastName("Kaiser")))), pageable);
List<Ancestor> ancestors = repository.findAll( Specification.where(AncestorRepository.gender(Gender.MALE)) .and(Specification.not(AncestorRepository.lastName("Kaiser")))), pageable); List<Ancestor> ancestors = repository.findAll(where(gender(Gender.MALE)).and(not(lastName("Kaiser")))), pageable);
List<Ancestor> ancestors = repository.findAll(
  Specification.where(AncestorRepository.gender(Gender.MALE))
    .and(Specification.not(AncestorRepository.lastName("Kaiser")))), pageable);

List<Ancestor> ancestors = repository.findAll(where(gender(Gender.MALE)).and(not(lastName("Kaiser")))), pageable);

Das Beispiel zeigt jedoch noch immer eine feste Suchanfrage. Im folgenden ein dynamisches Beispiel bei dem nach Geburtsort, Geburtsdatum, Vor-, Mittel- und Nachnamen gesucht werden kann. Im Gegensatz zum Beispiel am Anfang des Beitrags sind hier alle Suchparameter optional. Ist für ein Attribut kein Wert angegeben, so wird dieses Attribut nicht in die Suche einbezogen.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
Specification<Ancestor> specification = (ancestor, cq, cb) -> {
Predicate[] predicates = Stream.of(
firstName.map(x -> cb.like(ancestor.get("firstName"), "%" + x + "%")),
middleName.map(x -> cb.like(ancestor.get("middleName"), "%" + x + "%")),
lastName.map(x -> cb.like(ancestor.get("lastName"), "%" + x + "%")),
place.map(x -> cb.like(ancestor.get("birthPlace"), "%" + x + "%")),
end.map(x -> cb.lessThanOrEqualTo(ancestor.<LocalDate>get("birth"), x)),
start.map(x -> cb.greaterThanOrEqualTo(ancestor.<LocalDate>get("birth"), x))
).map(Optional::isPresent).map(Optional::get).toArray(Predicate[]::new);
return cb.length == 0 ? null : cb.and(predicates);
};
return repository.findAll(specification, pageable);
Specification<Ancestor> specification = (ancestor, cq, cb) -> { Predicate[] predicates = Stream.of( firstName.map(x -> cb.like(ancestor.get("firstName"), "%" + x + "%")), middleName.map(x -> cb.like(ancestor.get("middleName"), "%" + x + "%")), lastName.map(x -> cb.like(ancestor.get("lastName"), "%" + x + "%")), place.map(x -> cb.like(ancestor.get("birthPlace"), "%" + x + "%")), end.map(x -> cb.lessThanOrEqualTo(ancestor.<LocalDate>get("birth"), x)), start.map(x -> cb.greaterThanOrEqualTo(ancestor.<LocalDate>get("birth"), x)) ).map(Optional::isPresent).map(Optional::get).toArray(Predicate[]::new); return cb.length == 0 ? null : cb.and(predicates); }; return repository.findAll(specification, pageable);
Specification<Ancestor> specification = (ancestor, cq, cb) -> {
  Predicate[] predicates = Stream.of(
    firstName.map(x -> cb.like(ancestor.get("firstName"), "%" + x + "%")),
    middleName.map(x -> cb.like(ancestor.get("middleName"), "%" + x + "%")),
    lastName.map(x -> cb.like(ancestor.get("lastName"), "%" + x + "%")),
    place.map(x -> cb.like(ancestor.get("birthPlace"), "%" + x + "%")),
    end.map(x -> cb.lessThanOrEqualTo(ancestor.<LocalDate>get("birth"), x)),
    start.map(x -> cb.greaterThanOrEqualTo(ancestor.<LocalDate>get("birth"), x))
  ).map(Optional::isPresent).map(Optional::get).toArray(Predicate[]::new);
  return cb.length == 0 ? null : cb.and(predicates);
};
return repository.findAll(specification, pageable);

Die

Specification
Specification erzeugt aus den Suchparametern einen
Stream
Stream von
Optional<Predicate>
Optional<Predicate> Instanzen. Leere Instanzen werden herausgefiltert und aus den übriggebliebenen
Predicate
Predicate Instanzen ein Array erzeugt. Ist das Array leer dann liefert die
Specification
Specification null zurück, ansonsten ein
Predicate
Predicate, dass alle Instanzen aus dem Array mit \land verknüpft.

Da in diesem Beispiel

start
start und
end
end unabhängig voneinander sind, können in der Suche auch offene Intervalle betrachtet werden. Beispielsweise kann nach allen Geburtsdaten vor 1800 oder nach 1600 gesucht werden, wenn der betreffende Wert gesetzt wird und der jeweils andere undefiniert bleibt.

Damit ist dieser Beitrag über Spring Data JPA Specifications auch schon wieder an sein Ende gekommen. Aber bei alledem was möglich ist, sollte man sich immer das folgende Zitat von Pablo Picasso vergegenwärtigen. Denn häufig hilft keine verbesserte Suche, wenn der Anwender nichts hilfreiches finden kann.

“Das Geheimnis der Kunst liegt darin, dass man nicht sucht, sondern findet.”

Pablo Picasso

Leave a Comment