Hinweis:

TASK 4: Datenbankabfragen implementieren und Micronaut-Anwendung erstellen

In dieser Übung implementieren Sie Datenbankabfragen und erstellen lokal eine Micronaut-Anwendung, die sich mit Oracle Autonomous Database verbindet.

Voraussichtliche Zeit: 30 Minuten

Aufgabeninhalt

In dieser Aufgabe führen Sie folgende Schritte aus:

Schritt 1: Mikronaut-Datenentitys erstellen, die Oracle Database-Tabellen zugeordnet sind

In der vorherigen Aufgabe haben Sie das SQL-Skript hinzugefügt, das eine Tabelle mit dem Namen OWNER und eine Tabelle mit dem Namen PET nach der Ausführung erstellt. Als Nächstes müssen Sie Entity-Klassen definieren, mit denen Daten aus den Datenbanktabellen gelesen werden können.

  1. Erstellen Sie eine Entityklasse, die eine Owner im Package src/main/java/com/example darstellt. Klicken Sie mit der rechten Maustaste auf src/main/java/com/example, um das Inhaltsmenü einzublenden, wählen Sie "Neue Datei", benennen Sie sie Owner.java, und fügen Sie den folgenden Code ein:

    package com.example;
    
    import io.micronaut.core.annotation.Creator;
    import io.micronaut.data.annotation.GeneratedValue;
    import io.micronaut.data.annotation.Id;
    import io.micronaut.data.annotation.MappedEntity;
    
    @MappedEntity
    public class Owner {
    
      // The ID of the class uses a generated sequence value
      @Id
      @GeneratedValue
      private Long id;
      private final String name;
      private int age;
    
      // the constructor reads column values by the name of each constructor argument
      @Creator
      public Owner(String name) {
          this.name = name;
      }
    
      // each property of the class maps to a database column
      public int getAge() {
          return age;
      }
    
      public void setAge(int age) {
          this.age = age;
      }
    
      public String getName() {
          return name;
      }
    
      public Long getId() {
          return id;
      }
    
      public void setId(Long id) {
          this.id = id;
      }
    }
    

    Mit der Annotation @MappedEntity wird angegeben, dass die Entity einer Datenbanktabelle zugeordnet ist. Standardmäßig ist dies eine Tabelle, die denselben Namen wie die Klasse verwendet (in diesem Fall owner).

    Die Spalten der Tabelle werden durch jede Java-Eigenschaft dargestellt. Im obigen Fall wird eine id-Spalte verwendet, um den Primärschlüssel darzustellen, und @GeneratedValue richtet das Mapping so ein, dass eine identity-Spalte in Autonomous Database verwendet wird.

    Die Annotation @Creator wird für den Konstruktor verwendet, mit dem die zugeordnete Entity instanziiert wird. Außerdem werden erforderliche Spalten ausgedrückt. In diesem Fall ist die Spalte name erforderlich und unveränderbar, während die Spalte age nicht erforderlich ist und unabhängig mit dem Setter setAge festgelegt werden kann.

  2. Erstellen Sie eine Pet.java-Datei, die die Pet-Entity darstellt, um eine pet-Tabelle unter src/main/java/com/example zu modellieren:

    package com.example;
    
    import io.micronaut.core.annotation.Creator;
    import io.micronaut.core.annotation.Nullable;
    import io.micronaut.data.annotation.AutoPopulated;
    import io.micronaut.data.annotation.Id;
    import io.micronaut.data.annotation.MappedEntity;
    import io.micronaut.data.annotation.Relation;
    
    import java.util.UUID;
    
    @MappedEntity
    public class Pet {
    
        // This class uses an auto populated UUID for the primary key
        @Id
        @AutoPopulated
        private UUID id;
    
        private final String name;
    
        // A relation is defined between Pet and Owner
        @Relation(Relation.Kind.MANY_TO_ONE)
        private final Owner owner;
    
        private PetType type = PetType.DOG;
    
        // The constructor defines the columns to be read
        @Creator
        public Pet(String name, @Nullable Owner owner) {
            this.name = name;
            this.owner = owner;
        }
    
        public Owner getOwner() {
            return owner;
        }
    
        public String getName() {
            return name;
        }
    
        public UUID getId() {
            return id;
        }
    
        public void setId(UUID id) {
            this.id = id;
        }
    
        public PetType getType() {
            return type;
        }
    
        public void setType(PetType type) {
            this.type = type;
        }
    
        public enum PetType {
            DOG,
            CAT
        }
    }
    

    Beachten Sie, dass die Klasse Pet eine automatisch aufgefüllte UUID als Primärschlüssel verwendet, um unterschiedliche Ansätze bei der ID-Generierung darzustellen.

    Eine Beziehung zwischen der Klasse Pet und der Klasse Owner wird auch mit der Annotation @Relation(Relation.Kind.MANY_TO_ONE) definiert. Dies bedeutet, dass es sich um eine Eins-zu-Eins-Beziehung handelt.

    Danach ist es an der Zeit, zur Definition von Repository-Schnittstellen zu wechseln, um Abfragen zu implementieren.

Schritt 2: Mikronaut-Daten-Repositorys zum Implementieren von Abfragen definieren

Micronaut Data unterstützt das Konzept der Definition von Schnittstellen, die SQL-Abfragen automatisch bei der Kompilierung mithilfe des Daten-Repository-Musters implementieren. In diesem Abschnitt nutzen Sie diese Micronaut-Datenfunktion.

  1. Erstellen Sie einen separaten Ordner mit dem Namen repositories unter src/main/java/com/example.

  2. Definieren Sie eine neue Repository-Schnittstelle, die sich von CrudRepository erstreckt und mit @JdbcRepository mit dem Dialekt ORACLE in einer Datei mit dem Namen OwnerRepository.java versehen wird:

    package com.example.repositories;
    
    import com.example.Owner;
    import io.micronaut.data.jdbc.annotation.JdbcRepository;
    import io.micronaut.data.model.query.builder.sql.Dialect;
    import io.micronaut.data.repository.CrudRepository;
    
    import java.util.List;
    import java.util.Optional;
    
    // The @JdbcRepository annotation indicates the database dialect
    @JdbcRepository(dialect = Dialect.ORACLE)
    public interface OwnerRepository extends CrudRepository<Owner, Long> {
    
        @Override
        List<Owner> findAll();
    
        // This method will compute at compilation time a query such as
        // SELECT ID, NAME, AGE FROM OWNER WHERE NAME = ?
        Optional<Owner> findByName(String name);
    }
    

    Die Schnittstelle CrudRepository benötigt 2 generische Argumenttypen. Der erste Typ ist der Typ der Entity (in diesem Fall Owner), der zweite der Typ der ID (in diesem Fall Long).

    Die Schnittstelle CrudRepository definiert Methoden, mit denen Sie CRUD-Entitys mit den entsprechenden SQL-Einfügungen, -Auswahlen, -Updates und -Löschvorgängen erstellen, lesen, aktualisieren und aus der Datenbank löschen können, die zur Kompilierungszeit für Sie berechnet wurden. Weitere Informationen finden Sie im Javadoc für CrudRepository.

    Sie können Methoden innerhalb der Schnittstelle definieren, die JDBC-Abfragen ausführen und automatisch alle komplexen Details für Sie verarbeiten, wie die korrekte Transaktionssemantik (schreibgeschützte Transaktionen für Abfragen), die Abfrage ausführen und die Ergebnismenge der zuvor definierten Entityklasse Owner zuordnen.

    Die oben definierte Methode findByName erzeugt automatisch eine Abfrage wie SELECT ID, NAME, AGE FROM OWNER WHERE NAME = ? zur Kompilierungszeit.

    Weitere Informationen zu Abfragemethoden und den Abfragetypen, die Sie definieren können, finden Sie in der Dokumentation für Abfragemethoden in der Dokumentation zu Micronaut Data.

  3. Definieren Sie mit der vorhandenen OwnerRepository ein anderes Repository. Verwenden Sie dieses Mal ein Datenübertragungsobjekt (DTO), um eine optimierte Abfrage auszuführen. Zuerst müssen Sie die DTO-Klasse in einer Datei mit dem Namen NameDTO.java unter src/main/java/com/example/repositories erstellen:

    package com.example.repositories;
    
    import io.micronaut.core.annotation.Introspected;
    
    @Introspected
    public class NameDTO {
        private String name;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    

    Ein DTO ist ein einfacher POJO, mit dem Sie nur die Spalten auswählen können, die eine bestimmte Abfrage benötigt, wodurch eine optimiertere Abfrage erzeugt wird.

  4. Definieren Sie das Repository mit dem Namen PetRepository in einer Datei mit dem Namen PetRepository.java für die Entity Pet, die das DTO im selben Speicherort verwendet: src/main/java/com/example/repositories:

    package com.example.repositories;
    
    import com.example.Pet;
    import io.micronaut.data.annotation.Join;
    import io.micronaut.data.jdbc.annotation.JdbcRepository;
    import io.micronaut.data.model.query.builder.sql.Dialect;
    import io.micronaut.data.repository.PageableRepository;
    
    import java.util.List;
    import java.util.Optional;
    import java.util.UUID;
    
    @JdbcRepository(dialect = Dialect.ORACLE)
    public interface PetRepository extends PageableRepository<Pet, UUID> {
    
        List<NameDTO> list();
    
        @Join("owner")
        Optional<Pet> findByName(String name);
    }
    

    Notieren Sie sich die Methode list, die das DTO zurückgibt. Diese Methode wird bei der Kompilierung erneut für Sie implementiert. Anstatt jedoch alle Spalten der Tabelle Pet abzurufen, ruft sie nur die Spalte name und alle anderen Spalten ab, die Sie definieren können.

    Die Annotation @Join führt eine Abfrage und Instanziierung des verknüpften Objekts (Owner) durch und weist es dem Feld Owner der abgefragten Pet zu.

    Die Methode findByName ist auch interessant, da sie ein weiteres wichtiges Feature von Micronaut Data verwendet, das die Annotation @Join ist. Damit können Sie Join-Pfade angeben, sodass Sie genau die Daten abrufen können, die Sie über Datenbank-Joins benötigen. Dies führt zu wesentlich effizienteren Abfragen.

Setzen Sie die Daten-Repositorys ein, und geben Sie REST-Endpunkte an.

Schritt 3: Micronaut-Controller als REST-Endpunkte angeben

REST-Endpunkte in Micronaut sind einfach zu schreiben und werden als Controller (gemäß dem MVC-Muster) definiert.

  1. Erstellen Sie einen Ordner controllers unter src/main/java/com/example/.

  2. Definieren Sie eine neue OwnerController-Klasse in einer Datei mit dem Namen OwnerController.java:

    package com.example.controllers;
    
    import java.util.List;
    import java.util.Optional;
    
    import javax.validation.constraints.NotBlank;
    
    import com.example.Owner;
    import com.example.repositories.OwnerRepository;
    import io.micronaut.http.annotation.Controller;
    import io.micronaut.http.annotation.Get;
    import io.micronaut.scheduling.TaskExecutors;
    import io.micronaut.scheduling.annotation.ExecuteOn;
    
    @Controller("/owners")
    @ExecuteOn(TaskExecutors.IO)
    class OwnerController {
    
        private final OwnerRepository ownerRepository;
    
        OwnerController(OwnerRepository ownerRepository) {
            this.ownerRepository = ownerRepository;
        }
    
        @Get("/")
        List<Owner> all() {
            return ownerRepository.findAll();
        }
    
        @Get("/{name}")
        Optional<Owner> byName(@NotBlank String name) {
            return ownerRepository.findByName(name);
        }
    }
    

    Eine Controllerklasse wird mit der Annotation @Controller definiert, mit der Sie die Root-URI definieren können, der dem Controller zugeordnet wird (in diesem Fall /owners).

    Beachten Sie die Annotation @ExecuteOn, mit der Micronaut mitgeteilt wird, dass der Controller I/O-Kommunikation mit einer Datenbank ausführt. Daher sollten Vorgänge auf dem I/O-Threadpool ausgeführt werden.

    Die Klasse OwnerController verwendet Micronaut Dependency Injection, um eine Referenz auf die zuvor definierte OwnerRepository-Repository-Schnittstelle abzurufen und zwei Endpunkte zu implementieren:

    • /: Der Root-Endpunkt listet alle Eigentümer auf
    • /{name}: Der zweite Endpunkt verwendet eine URI-Vorlage, damit ein Eigentümer nach Name gesucht werden kann. Der Wert der URI-Variablen {name} wird als Parameter für die Methode byName angegeben.
  3. Definieren Sie als Nächstes einen weiteren REST-Endpunkt mit dem Namen PetController in einer Datei mit dem Namen PetController.java unter src/main/java/com/example/controllers:

    package com.example.controllers;
    
    import java.util.List;
    import java.util.Optional;
    
    import com.example.repositories.NameDTO;
    import com.example.Pet;
    import com.example.repositories.PetRepository;
    import io.micronaut.http.annotation.Controller;
    import io.micronaut.http.annotation.Get;
    import io.micronaut.scheduling.TaskExecutors;
    import io.micronaut.scheduling.annotation.ExecuteOn;
    
    @ExecuteOn(TaskExecutors.IO)
    @Controller("/pets")
    class PetController {
    
        private final PetRepository petRepository;
    
        PetController(PetRepository petRepository) {
            this.petRepository = petRepository;
        }
    
        @Get("/")
        List<NameDTO> all() {
            return petRepository.list();
        }
    
        @Get("/{name}")
        Optional<Pet> byName(String name) {
            return petRepository.findByName(name);
        }
    }
    

    Diesmal wird die PetRepository injiziert, um eine Liste von Haustieren und Haustieren nach Namen anzugeben.

Schritt 4: Daten beim Anwendungsstart auffüllen

Im nächsten Schritt werden beim Start einige Anwendungsdaten aufgefüllt. Dazu können Sie Micronaut-Anwendungsereignisse verwenden.

Öffnen Sie die Klasse src/main/java/com/example/Application.java, und ersetzen Sie den ursprünglichen Dateiinhalt durch Folgendes:

package com.example;

import com.example.repositories.OwnerRepository;
import com.example.repositories.PetRepository;
import io.micronaut.context.event.StartupEvent;
import io.micronaut.runtime.Micronaut;
import io.micronaut.runtime.event.annotation.EventListener;

import jakarta.inject.Singleton;
import javax.transaction.Transactional;
import java.util.Arrays;

@Singleton
public class Application {
    private final OwnerRepository ownerRepository;
    private final PetRepository petRepository;

    Application(OwnerRepository ownerRepository, PetRepository petRepository) {
        this.ownerRepository = ownerRepository;
        this.petRepository = petRepository;
    }

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }

    @EventListener
    @Transactional
    void init(StartupEvent event) {
        // clear out any existing data
        petRepository.deleteAll();
        ownerRepository.deleteAll();

        // create data
        Owner fred = new Owner("Fred");
        fred.setAge(45);
        Owner barney = new Owner("Barney");
        barney.setAge(40);
        ownerRepository.saveAll(Arrays.asList(fred, barney));

        Pet dino = new Pet("Dino", fred);
        Pet bp = new Pet("Baby Puss", fred);
        bp.setType(Pet.PetType.CAT);
        Pet hoppy = new Pet("Hoppy", barney);

        petRepository.saveAll(Arrays.asList(dino, bp, hoppy));
    }
}

Beachten Sie, dass der Konstruktor in Abhängigkeit geändert wird, um die Repository-Definitionen zu injizieren, damit Daten dauerhaft gespeichert werden können.

Schließlich wird die Methode init mit @EventListener mit einem Argument versehen, um eine StartupEvent zu empfangen. Dieses Ereignis wird aufgerufen, sobald die Anwendung hochgefahren und gestartet ist. Es kann verwendet werden, um Daten zu persistieren, wenn die Anwendung dazu bereit ist.

Im weiteren Beispiel wird gezeigt, wie einige Entitys mit der Methode saveAll der Schnittstelle CrudRepository gespeichert werden.

Beachten Sie, dass javax.transaction.Transactional für die Methode deklariert wird, die sicherstellt, dass Micronaut Data die Ausführung der Methode init in einer JDBC-Transaktion wrappt, die zurückgesetzt wird, wenn eine Ausnahme während der Ausführung der Methode auftritt.

Schritt 5: Integrationstests für die Micronaut-Anwendung ausführen

Die Anwendung wurde bereits mit einem einzelnen Test eingerichtet, der prüft, ob die Anwendung erfolgreich hochgefahren werden kann (und daher die Logik der im vorherigen Abschnitt definierten Methode init testet).

  1. Gehen Sie in der oberen Navigationsleiste zu Terminal und dann zu New Terminal.

  2. Führen Sie das Ziel test aus, um Tests auszuführen:

    ./mvnw test
    

    Zeigen Sie die Dateistruktur an, und führen Sie Tests in einem Terminalfenster im VS-Code aus

    Wenn Sie Gradle verwenden, führen Sie alternativ die Aufgabe test aus:

    ./gradlew test
    

    Die Meldung BUILD SUCCESS sollte am Ende der Testausführung angezeigt werden.

Sie können jetzt mit der nächsten Aufgabe fortfahren.

Weitere Informationen