Keine Ahnung von MongoDB? Dann nimm Spring Data!

Spring Data vereinheitlicht für Euch den Zugriff auf die Daten. Es unterstützt klassische, relationale und NoSQL Datenbanken. Hier zeige ich wie man MongoDB nutzen kann, ohne es (im Detail) verstehen zu müssen.


MongoDB (lokal) starten

Zum Kennenlernen von Spring Data und zum Ausprobieren von MongoDB braucht man kein aufwändiges Setup. Wenn man Cloud-Dienste nutzen kann, kann man sich MongoDB als Service in Azure, AWS oder der Open Telekom Cloud bereitstellen lassen, siehe z.B. hier:
https://open-telekom-cloud.com/de/produkte-services/document-database-service

MongoDB selbst bietet mit MongoDB Atlas auch einen eigenen Cloud-Service an, den man kostenlos ausprobieren kann: https://www.mongodb.com/try

Ich habe mich für eine lokale Variante entschieden, in dem ich einen MongoDB Docker Container starte.
Hier eine kurzer Überblick wie das geht:
  1. Docker für Windows installieren, siehe: 
    https://docs.docker.com/docker-for-windows/install/
    Docker funktioniert mit Linux mindestens genau so gut, dann bräuchte ich aber noch eine Linux VM und die Anleitung wäre länger 😜  
  2. Nach erfolgreicher Docker Installation, kann man eigentlich direkt den MongoDB Container starten.
    Dazu z.B. in PowerShell folgenden Befehl ausführen:
    docker run -d -p 27017:27017 --name mongodb mongo

Kurzer Docker Exkurs

Was macht der benutzte Docker Befehl genau?
docker run -d -p 27017:27017 --name mongodb mongo
  • "docker run" ist der Start-Befehl für einen Container. Damit es funktioniert muss man mindestens noch den Namen des Docker Images angeben, welches im Container ausgeführt werden soll. Unser Image heißt "mongo" und wird über das Internet aus dem Docker Hub geladen:
    https://hub.docker.com/_/mongo
  • Damit der Docker Container im Hintergrund läuft bzw. von der Shell abgelöst ist, gibt man die Option "-d" (detach) an. 
  • "-p 27017:27017" macht das Port Mapping von unserem Rechner in den Docker-Container. Im mongo Image ist festgelegt, dass auf Port 27017 gelauscht wird.
  • "--name mongodb" gibt unserem Container den festen Namen mongodb. Das ist optional, vereinfacht aber das Stoppen, Starten oder Löschen des Containers, da man den Container über den selbst vergebenen Namen ansprechen kann. 
Um zu überprüfen, ob der Container wirklich läuft kann man diesen Befehl verwenden:
docker ps -a

Wenn alles funktioniert hat, sollte es so aussehen:
PS C:\> docker run -d -p 27017:27017 --name mongodb mongo
6b5978942ae35f55848fa781ec48d616f12d9e518352affa97636a3628ba3ec0

PS C:\> docker ps -a
CONTAINER ID    IMAGE    STATUS        PORTS
6b5978942ae3    mongo    Up 6 seconds  0.0.0.0:27017->27017/tcp
(Die Ausgabe des docker ps -a Befehls ist hier gekürzt)

Weitere Infos zu Docker findet ihr hier: https://docs.docker.com/

Ich hatte dieses Buch zu Docker gelesen.


Spring Data

Neues Spring Data Projekt erstellen

Der schnellste Weg zum neuen Spring Data Projekt mit MongoDB führt über Spring Boot. Wie schon in meinen anderen Blog-Posts gezeigt, können wir den Spring Boot Wizard nutzen, um ein neues Spring Data MongoDB Projekt zu erstellen. Das funktioniert genauso wie in diesem Blog-Post gezeigt:
microservices-mit-spring-boot-erstellen.html

💡 Einziger Unterschied im 2 Schritt des Wizards "New Spring Starter Project Dependencies" müssen wir "NoSQL" => "Spring Data MongoDB" auswählen - weitere Dependencies brauchen wir in diesem Tutorial nicht.
Spring Starter Wizard - Schritt 2

Spring Data Bibliothek hinzufügen

Wenn man in einem bestehenden Projekt Spring Data für MongoDB hinzufügen möchte, muss man nur eine neue Maven Dependency eintragen:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

Die nötigen Konfigurationen zeige ich in den nächsten Abschnitten.

Verbindung zu MongoDB konfigurieren

Spring Data erwartet, dass die Verbindung zu MongoDB in den Spring Properties gemacht wird. Falls ihr mit Spring Boot ein neues Spring Data Projekt generiert habt, öffnet diese Datei: application.properties. Dort tragt ihr folgende Konfiguration ein: 
    
    spring.data.mongodb.host=localhost
    spring.data.mongodb.port=27017
    spring.data.mongodb.database=local

Im mongo Docker Image wird kein Username und Passwort benötigt, daher sind hier Host-Addresse, -Port und Datenbank-Name ausreichend.
Weitere Spring Data Properties findet ihr hier:
https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#data-properties

DAO Klasse erstellen

Um JSON-Dokumente aus der MongoDB auszulesen und in Java Objekte zu überführen, brauchen wir DAO-Klassen. DAO-Klassen sehen dank Spring Data aus wie einfache POJO Klassen mit ein paar Annotationen. Der Boilerplate-Code zum Lesen und Schreiben wird durch die Spring Data Repositories gemanagt. Repositories stelle ich im nächsten Abschnitt vor.
Hier im Beispiel wollen wir Mitarbeiter einer Firma aus der NoSQL Datenbank lesen und schreiben, daher benötigen wir eine einfache POJO Klasse Employee, die um einige Annotationen ergänzt ist:

import java.time.Instant;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;

@Document(collection = "Employee")
public class Employee {
    @Id
    private String id;
 
    @Indexed(unique = false)
    @Field(value = "Emp_No")
    private String empNo;
 
    private String fullName;
 
    @Field(value = "Hire_Date")
    private Instant hireDate;

    // Getter and Setter methods for all attributes...

  • @Document mappt die Klasse auf eine Collection in der Mongo Datenbank local - local hatten wir in unserer Verbindungs-Konfiguration (application.properties) angegeben. local ist eine Standard Datenbank von MongoDB und wird hier nur verwendet, um das Tutorial kürzer zu halten.
  • @Field mappt die einzelnen Attribute unserer POJO Klasse auf Keys in den JSON Dokumenten innerhalb der Dokumenten Datenbank. Sofern Key- und Attribute-Name dieselbe Schreibweise haben, kann man die @Field Annotation auch weglassen, siehe "fullName".
  • @Indexed sagt MongoDB, dass es für dieses Attribut einen Index aufbauen soll, der Suchanfragen (in unserem Fall nach empNo) beschleunigt. In diesem Index müssen die indexierten Werte nicht eindeutig sein (unique = false) - Duplikate sind erlaubt.
  • @Id ist eine eindeutige ID - hier werden Duplikate direkt beim Schreibversuch abgelehnt. Ist hier kein Wert gesetzt, so wird er automatisch generiert.
Eine valide JSON-Struktur sieht z.B. so aus:
    {
        "Emp_No":"456",
        "fullName":"Elmar Brauch",
        "Hire_Date":"2010-08-01T10:15:30.000Z"
    }

MongoRepository erstellen

Spring Data hat Repositories eingeführt, um die Menge an Boilerplate-Code beim Zugriff auf Datenbanken erheblich zu reduzieren. Anstelle der DAO-Klasse gibt es einfache POJO Klassen mit Annotationen. Um Lese-, Schreib-, Update- oder Lösch-Operationen an die Datenbank zu schicken, verwendet man in Spring Data Repositories. Das besondere an den Repositories ist, dass sie nur ein Interface sind, deren Implementierung automatisch von Spring Data bereitgestellt wird. Dadurch muss der Entwickler keinen Standard-Code (Boilerplate-Code) zum Interagieren mit der Datenbank schreiben. Diese Repository Interfaces müssen das org.springframework.data.repository.Repository Interface erweitern, damit Spring die Implementierung bereitstellen kann.

SpringData bietet bereits einige Spezialisierungen des Repository Interfaces an, da wir hier eine MongoDB nutzen wollen, verwenden wir das Interface
MongoRepository<T, ID>
  • Der Generic T steht für die DAO bzw. POJO Klasse, welche mittels dieses Repositories in der NoSQL Datenbank verarbeitet wird.
  • Generic ID ist der Typ der ID, welcher an diesem POJO verwendet wird und entspricht dem Typ des mit @Id annotierten Attributes.
Für unsere Employee-Klasse erstellen wir nun folgendes MongoRepository:
    import java.time.Instant;
    import java.util.List;
    import org.springframework.data.mongodb.repository.MongoRepository;

    //Long: Type of Employee ID.
    public interface EmployeeRepository 
            extends MongoRepository<Employee, Long> {
 
        Employee findByEmpNo(String empNo);
 
        List<Employee> findByFullNameLike(String fullName);
 
        List<Employee> findByHireDateGreaterThan
                (Instant hireDate);
    }

  • Auch ohne Methoden würde das EmployeeRepository bereits einige Lese-, Schreib- und Lösch-Operationen anbieten, z.B.:
    • Employee save(Employee entity);
      Zum Speichern eines Employee-Objektes in der Datenbank.
    • Employee findById(String id);
      Zum Lesen einer Employee-Instanz aus der Datenbank, identifiziert anhand seines ID Attributes (siehe @Id Annotation in Employee-Klasse).
    • void delete(Employee entity);
      Zum Löschen des übergebenen Employees in der Datenbank.
  • Die zuvor genannten Methoden stammen aus dem CrudRepository, welches ebenfalls eine Spezialisierung von Repository ist. CrudRepository ist gleichzeitig eine Superklasse von MongoRepository, daher erbt MongoRepository und somit auch unser EmployeeRepository dessen Methoden.
    Würden wir statt MongoDB eine relationale Datenbank (z.B. PostgreSQL) nutzen, würde Spring Data uns diese Methoden für relationale Datenbanken bereitstellen und wir sparen uns wieder das Schreiben von eigenem (Boilerplate-)Code. 😃
  • Das EmployeeRepository hat 3 findBy Methoden, die wir für unsere "Geschäftslogik" benötigen. Spring Data hat einen Query Derivation Mechanismus, der anhand von Schlüsselwörtern in Methodennamen und Signatur eine Implementierung ableitet:
    • findByEmpNo müssen wir also nicht selbst implementieren, sondern Spring Data weiß anhand der Methoden Deklaration, dass der Eingabeparameter dem Key Emp_No im JSON entspricht (siehe @Field Annotation in Employee Klasse).
    • findByFullNameLike hat noch ein weiteres Schlüsselwort "Like" und ist damit in der Lage nach Teil-Strings in der Datenbank zu suchen. Auch hier ist keine eigene Implementierung nötig.
    • findByHireDateGreaterThan verwendet zusätzlich das Schlüsselwort "GreaterThan", um die Liste aller Employee Objekte zu liefern, die ein späteres Einstellungsdatum haben als im übergebenen Parameter geliefert wurde.
  • Spring Data bietet noch viele weitere Möglichkeiten der Query Derivation, dazu verweise ich aber auf https://spring.io/projects/spring-data

MongoDB und Spring Data Repository testen

Im Sinne von Test-Driven-Development habe ich mich für einen JUnit Test zum Ausprobieren und Testen meines Repositories entschieden. Da wir hier nur ein Demo-Projekt entwickelt haben, hat mein JUnit-Test keinen Anspruch auf professionelles Testen - mir ging es nur darum zu sehen, dass die Methoden im EmployeeRepository funktionieren. Daher sieht mein JUnit Test so aus:

@SpringBootTest
class MongoDemoApplicationTests {

    @Autowired
    private EmployeeRepository employeeRepo;
    @BeforeEach
    void initDatabase() {
     employeeRepo.deleteAll();
     var uwe = new Employee();
     uwe.setEmpNo("123");
     uwe.setFullName("Uwe Mustermann");
     uwe.setHireDate(Instant.now());
     var elmar = new Employee();
     elmar.setEmpNo("456");
     elmar.setFullName("Elmar Brauch");
     elmar.setHireDate(
            Instant.parse("2010-08-01T10:15:30.00Z"));
employeeRepo.insert(List.of(uwe, elmar));
    }
    @Test
    void addOneEmployee() {
     var bill = new Employee();
     bill.setFullName("Bill Gates");
     employeeRepo.insert(bill);
     assertEquals(3, employeeRepo.count());
    }
    @Test
    void findByName() {
     var result = employeeRepo
                .findByFullNameLike("Elmar");
assertEquals(1, result.size());
assertEquals("456", result.get(0).getEmpNo());
    }
    @Test
    void findByHireDate() {
     assertTrue(employeeRepo.findByHireDateGreaterThan(
            Instant.now()).isEmpty());
var yesterday = Instant.now()
            .minus(1, ChronoUnit.DAYS);
assertEquals(1, employeeRepo
            .findByHireDateGreaterThan(yesterday).size());
    }
}

  • @SpringBootTest sorgt dafür, dass die Klasse ein JUnit Test ist und Zugriff auf den Spring ApplicationContext hat.
  • Mit @Autowired wird unser EmployeeRepository aus dem ApplicationContext automatisch referenziert, damit wir es in den folgenden Tests benutzen können.
  • Die mit @BeforeEach annotierte Methode wird vor jedem der folgenden Tests ausgeführt. Wir löschen zuerst alle Employee Dokumente in der MongoDB. Dann erstellen wir 2 Employee Objekte, die wir in unseren Tests nutzen wollen und schreiben diese beiden Employee Objekte in die Mongo Datenbank. Auf diese Weise beeinflussen die Tests sich nicht gegenseitig und jeder Test startet mit dem gleichen Zustand in der Datenbank.
  •  Im addOneEmployee Test erstellen wir einen dritten Employee und schreiben ihn in die Datenbank ( employeeRepo.insert(bill); ). Danach testen wir, ob nun 3 Employee Dokumente in MongoDB gespeichert sind - die dazu verwendete count Methode wurde ebenfalls von Spring Data für uns bereitgestellt.
  • In den beiden anderen Tests verwenden wir die beiden findBy-Methoden aus unserem EmployeeRepository und stellen sicher, dass sie wie erwartet funktionieren.
Um weitere Spring Data Funktionen kennen zu lernen, könnt ihr nun das EmployeeRepository erweitern und es mit Hilfe der Test-Klasse testen. Wenn ihr in die MongoDB mit einem Client reinschauen wollt, so könnt ihr z.B. MongoDB Compass verwenden, siehe:
https://www.mongodb.com/try/download/compass

Datenbank Client MongoDB Compass


Fazit & Ausblick

Ich habe euch gezeigt, wie man einfach und schnell mit Docker eine MongoDB zum Entwickeln startet. Außerdem haben wir gesehen, wie man Spring Data nutzt, um mit Datenbanken zu interagieren, ohne spezifisches Wissen zu haben. Nun habt ihr dem Punkt erreicht, an dem ihr sowohl euer Spring Data als auch euer MongoDB Wissen durch Ausprobieren und Spielen mit den Technologien ausbauen und vertiefen könnt.
Den kompletten Code aus diesem Post findet ihr auf GitHub:
https://github.com/elmar-brauch/mongo

Ich hoffe, dass ihr gut folgen konntet. Falls nicht, könnt ihr mir gerne Fragen oder Verbesserungswünsche per Kommentar hinterlassen.

Gebt mir auch gerne Bescheid, wenn ihr Interesse an einer möglichen Folge-Session habt:
  • Spring Data und eine relationale Datenbank (PostgreSQL, MySQL, MariaDB, etc)
  • Vertiefung Spring Data - wie macht man komplexe Queries?

Dieses Buch habe ich zu Spring Data gelesen.

Kommentare

Falls ihr MongoDB Atlas verwendet, könnt ihr die Verbindung zur Datenbank in der application.properties Datei mit nur einer Zeile konfigurieren. Das sieht dann z.B. so aus:

spring.data.mongodb.uri=mongodb+srv://mongo:@cluster0.clla8.mongodb.net/test

Beliebte Posts aus diesem Blog

OpenID Connect mit Spring Boot 3

Authentifizierung in Web-Anwendungen mit Spring Security 6

Reaktive REST-Webservices mit Spring WebFlux