LIKE Queries mit Spring Boot

Würde man ein Ranking der beliebtesten Beispielprojekte machen – ich bin mir sicher, dass die TODO-Liste ganz weit vorne landen würde. Selbst nutze ich das Beispiel gerne, um unterschiedlichste Technologien, Patterns und Ansätze praktisch vorzuführen.

Nachdem wir in einem früheren Blogpost schon einen Endpunkt aufgesetzt haben, der eine Liste von Tasks liefert, wollen wir heute die Lücke zu einem halbwegs sinnvollen Produkt zumindest ein Stück schließen. Beim Einstieg zu REST Services haben wir der Einfachheit halber eine fixe Liste von Tasks im Code generiert und einfach zurückgegeben. Zur Illustration hat das genügt, heute wollen wir das Beispiel um die Funktionalität zur Freitextsuche erweitern.

Struktureller Unterbau

Wer das Vergnügen hatte in Studium oder Ausbildung intensiv SQL zu lernen wird sofort hellhörig geworden sein: Freitext-Suche, da war doch was mit LIKE, oder? Es gab so ein paar spezielle Platzhalter, aber an sich war das doch eigentlich nicht spektakulär. Genau! Daher nutzen wir das heute, um unser bestehendes Beispiel zu erweitern.

Um SQL, und damit auch unsere Query mit LIKE ausführen zu können, benötigen wir eine Datenbank. Um nicht zu viel Zeit mit der lokalen Konfiguration zu verschwenden nutzen wir hier H2 als In-Memory-Datenbank. Wer sich über die Hintergründe und weitere Anwendungsfälle informieren möchte, dem sei der entsprechende Post ans Herz gelegt.

Auch wenn mancher SQL-Romantiker ein wenig enttäuscht sein wird, unsere Queries müssen wir nicht mehr selbst schreiben. Zumindest nicht für so einen halbwegs trivialen Anwendungsfall, wie wir ihn heute mit LIKE zur Freitextsuche umsetzen möchten. Spring Boot hat eine hervorragende Anbindung an die Java Persistence Architecture (JPA), die so gut wie alles rund um den Zugriff für uns abstrahiert. Um JPA mit einer sauber aufgesetzten relationalen Datenbank an die Grenze zu bringen, braucht es schon spezielle Anforderungen (aber es geht…). Dann ist es Zeit für die Romantiker, denn in der allergrößten Not, dürfen wir natürlich auch echtes, hartes SQL schreiben. Aber heute nutzen wir einfach „nur“ die Abstraktionen.

Vorbereitung des Datenzugriffes

Um unseren Anwendungsfall der Freitextsuche zu implementieren, habe ich das Beispiel aus dem Einstiegspost strukturell etwas aufgebohrt (Download Beispielprojekt tasklist).

Zum einen habe ich in der Datei src/main/resources/application.properties die Konfiguration für die Datenbank hinzugefügt. Zum anderen habe ich die Klasse Task extrahiert. Durch die etwas seltsam anmutenden Annotationen wird die Klasse zu einer JPA-Entity, also einfach gesprochen zu einer 1:1 Repräsentation einer Tabelle, bzw. einer Zeile in einer Datenbanktabelle.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "task")
public class Task {

     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "id")
     private long id;

     @Column(name = "title")
     private String title;
}

Dafür sorgen @Entity und @Table. Die Annotation @Column kümmert sich um die Zuordnung zwischen den Feldern der Java-Klasse und der Datenbanktabelle, @Id und @GeneratedValue geben den Primärschlüssel an.

Das war jetzt glaube ich der kürzeste Crashkurs in JPA, den es auf der Welt gibt ;-)

Etwas unterschlagen habe ich noch alle weiteren Annotationen an der Klasse. Diese kommen aus der Bibliothek Lombok und nehmen uns eigentlich nur Schreibarbeit ab: Sie generieren automatisch die Getter und Setter, Konstruktoren, equals und hashCode Methoden und noch ein paar Dinge mehr. Tja, die Zeiten, als man Auszubildende mit dieser Schreibarbeit beschäftigt halten konnte, sind wohl vorbei…

Zugriff!

Ein sehr unscheinbares Interface habe ich bisher unterschlagen: Das TaskRepository.

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
}

Nur aufgrund der Tatsache, dass hier rein gar nichts implementiert ist, sollte man diese Komponente (durch die @Repository Annotation wird das Interface zu einer) nicht unterschätzen. Spring Boot ist manchmal Magie…

Kurz gesagt: Über dieses Interface können wir nachher sehr komfortabel unsere Queries mit dem gewünschten LIKE abfeuern, ohne auch nur ein bisschen SQL schreiben zu müssen. Wie das geht, sehen wir uns gleich an.

Wer sich es nicht erwarten kann, findet in der Klasse DemoDataProvider einen kurzen Eindruck, wie mit dem Repository gearbeitet werden kann. Die Klasse ist elegant in den Startprozess eingehängt und erzeugt einfach bei jeder Ausführung einen Satz von Testdaten, damit wir gleich etwas Futter für unsere LIKE Versuche haben.

Nochmal Theorie

Wie war das also noch einmal mit den LIKE Queries und speziell mit diesen Wildcards? Für unseren Anwendungsfall der Freitextsuche würden wir in SQL eine Query in dieser Richtung schreiben:

SELECT * FROM task WHERE LOWER(title) LIKE ‘%clean%‘

Der Suchbegriff ist also „clean“ und da wir ihn an beliebiger Wortposition finden wollen, fügen wir das Prozentzeichen als Wildcard für „irgendeine Buchstabenkombination oder gar nichts“ hinzu. Damit findet die Query mit LIKE den Suchbegriff sowohl am Anfang, wie am Ende und irgendwo mitten im Titel des Tasks.

Die Funktion LOWER angewendet auf die Spalte title führt dazu, dass die Suche case-insensitive wird, also Groß- und Kleinschreibung ignoriert. Zumindest so lange wir daran denken, den Suchbegriff auch in Kleinbuchstaben zu übergeben…

Also, in SQL wären wir so weit, aber wie machen wir das jetzt mit Spring Boot?

Abakadabra – Zeit für die Spring Boot Magie!

Bitte nicht enttäuscht sein, über das, was nun folgt. Ich verspreche, dass es auch noch Anwendungsfälle gibt, in denen man sich richtig anstrengen muss und auch mit JPA kann man noch richtig programmieren. Wirklich.

Aber unsere LIKE Query bekommen wir einfach geschenkt… Wir erweitern das Interface TaskRepository:

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
     List<Task> findByTitleLikeIgnoreCase(String query);
}

Fertig? Ja, fertig! Wer erwartet, dass wir jetzt noch eine Implementierung für die definierte Methode angeben müssen, irrt. Dadurch, dass wir uns an gewisse Namenskonventionen halten, implementiert Spring Boot uns unsere Klasse tatsächlich automatisch, wenn wir das Interface an einer anderen Stelle per @Autowired einbinden. Auf den ersten Blick überfordert das leicht, ist aber für die tägliche Arbeit sehr praktisch.

Nur eine kurze Erklärung, was hier im Hintergrund passiert, das ganze Konzept nennt sich Query Lookup Strategy und ist unter diesem Link knapp aber gut beschrieben.

Die Einleitung findBy signalisiert ein SELECT, die Tabelle weiß das Repository aus seinem Typparameter Task, der ja wiederum mit der @Table Annotation auf die Tabelle gebunden ist. Der Abschnitt TitleLikeIgnoreCase bedeutet, frei „übersetzt“ dass der erste Parameter, der an die Java Funktion übergeben wird, mittels eines case-insensitiven LIKES mit der Spalte title verglichen werden soll. Also genau das, was wir oben schon in SQL formuliert hatten.

Und wie bekommen wir das in unseren Service?

Ein kleiner Baustein fehlt noch, dazu schauen wir uns die angepasste Controllerklasse an:

@Controller
@RequestMapping("tasks")
public class TaskController {

     @Autowired
     private TaskRepository taskRepository;

     @GetMapping
     public ResponseEntity<List<Task>> getTasks(@RequestParam String query) {
          query = "%" + query.toLowerCase() + "%";

          List<Task> taskList = taskRepository.findByTitleLikeIgnoreCase(query);

          return ResponseEntity.ok(taskList);
     }
}

Zum einen holen wir uns, wie angekündigt, per Dependency-Injection eine Instanz des Repositories. Zum anderen haben wir unsere GET Methode für die Tasks so erweitert, dass sie einen Request-Parameter namens query erwartet.

Und nun dürfen wir doch noch ein ganz klein wenig programmieren… Das Hinzufügen der Wildcards machen wir nach guter alter Schule von Hand. Psst… Auch diese Zeile könnten wir uns sparen, wenn wir in der Repository-Methode Containing anstatt Like verwenden würden… Dann macht Spring Boot das für uns. Aber das ist dann vielleicht für den Einstieg zu viel Abstraktion. Schließlich wollten wir ja ein LIKE nachbauen, dafür ist das so sicherlich anschaulicher.

Naja, ansonsten passiert nicht viel spannendes. Wenn wir das Projekt starten (siehe dazu auch die Anleitung im ersten Blogpost zum REST Service), dann können wir im Browser ein wenig herumspielen. Dazu rufen wir einfach die folgende URL auf:

http://localhost:8080/tasks?query=

Ergebnis sind erstmal alle meine (zugegebenermaßen sinnlosen) Tasks aus dem Testdatenset. Wenn wir jetzt beginnen hinten richtige Suchbegriffe anzuhängen, sehen wir, dass unsere Freitextsuche wie gewünscht funktioniert!

Case-insensitive und unabhängig von der Position im Titel – ein Traum!

Fazit

Zugegebenermaßen sind wir auch mit diesem Beispiel noch weit davon entfernt, einen sinnvollen REST-basierten Webservice implementiert zu haben. Aber wir haben gesehen, dass sich Standardanwendungsfälle mit minimalem eigenem Coding-Aufwand umsetzen lassen.

Warum das so toll ist? Code smells… Jede eigene Code-Zeile muss gewartet werden, kann Fehler enthalten und sollte, sofern möglich, durch eine Bibliotheksfunktion ersetzt werden. Das sorgt vor allem durch singende Wartungsaufwände für nachhaltige Software.

Hier liegt, bei aller Magie, das große Plus von Spring Boot: Das Framework schenkt uns so viel Grundfunktionalität, dass wir uns wirklich auf unsere fachlichen Anforderungen konzentrieren können. Auch wenn es heute nur um eine triviale LIKE Query ging ;-)

Spring-Boot News

Bleibe auf dem aktuellsten Stand mit unseren kostenlosen Spring-Boot updates. So wirst Du direkt informiert, wenn wir einen neuen Artikel veröffentlichen.
Kein Spam, kein Bullshit, nur Spring-Boot Insider-Wissen, versprochen.

Noch keine Kommentare vorhanden.

Was denkst du?

© 2020 Mischok