Nota:

TASK 4: Implementa le query di database e crea un'applicazione Micronaut

In questo laboratorio implementerai le query di database e creerai un'applicazione Micronaut locale che si connette a Oracle Autonomous Database.

Tempo stimato: 30 minuti

Contenuto task

In questo task sarà possibile:

Passo 1: creare entità di dati Micronaut mappate alle tabelle di Oracle Database

Nel task precedente è stato aggiunto lo script SQL che creerebbe una tabella denominata OWNER e una tabella denominata PET una volta eseguita. Successivamente è necessario definire le classi di entità che possono essere utilizzate per leggere i dati dalle tabelle di database.

  1. Creare una classe di entità che rappresenterà un Owner nel pacchetto src/main/java/com/example. Fare clic con il pulsante destro del mouse su src/main/java/com/example per espandere il menu del contenuto, selezionare Nuovo file, assegnare il nome Owner.java e incollare il codice seguente:

    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;
      }
    }
    

    L'annotazione @MappedEntity viene utilizzata per indicare che l'entità è mappata a una tabella di database. Per impostazione predefinita, si tratta di una tabella che utilizza lo stesso nome della classe (in questo caso owner).

    Le colonne della tabella sono rappresentate da ogni proprietà Java. Nel caso precedente verrà utilizzata una colonna id per rappresentare la chiave primaria e @GeneratedValue imposterà il mapping per utilizzare una colonna identity in Autonomous Database.

    L'annotazione @Creator viene utilizzata nel costruttore che verrà utilizzata per creare un'istanza dell'entità mappata ed è utilizzata anche per esprimere le colonne richieste. In questo caso la colonna name è obbligatoria e immutabile mentre la colonna age non è obbligatoria e può essere impostata in modo indipendente utilizzando il setter setAge.

  2. Creare un file Pet.java che rappresenterà l'entità Pet per modellare una tabella pet in src/main/java/com/example:

    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
        }
    }
    

    Si noti che la classe Pet utilizza automaticamente UUID popolata come chiave primaria per dimostrare gli approcci diversi alla generazione degli ID.

    Una relazione tra la classe Pet e la classe Owner viene definita anche mediante l'annotazione @Relation(Relation.Kind.MANY_TO_ONE), a indicare che si tratta di una relazione molti-a-uno.

    A tal fine, è tempo di passare alla definizione delle interfacce di repository per implementare le query.

Passo 2: definire i repository di dati Micronaut per implementare le query.

I dati di Micronaut supportano la nozione di definizione di interfacce che implementano automaticamente query SQL in fase di compilazione utilizzando il pattern del repository di dati. In questa sezione potrete usufruire di questa funzione di dati Micronaut.

  1. Creare una cartella separata denominata repositories in src/main/java/com/example.

  2. Definire una nuova interfaccia di repository che si estende da CrudRepository e viene annotata con @JdbcRepository utilizzando il dialetto ORACLE in un file denominato OwnerRepository.java:

    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);
    }
    

    L'interfaccia CrudRepository utilizza due tipi di argomenti generici. Il primo è il tipo di entità (in questo caso Owner) e il secondo è il tipo di ID (in questo caso Long).

    L'interfaccia CrudRepository definisce metodi che consentono di creare, leggere, aggiornare ed eliminare entità (CRUD) dal database con gli inserimenti, le selezioni, gli aggiornamenti e le eliminazioni SQL appropriati calcolati in fase di compilazione. Per ulteriori informazioni, vedere Javadoc per CrudRepository.

    È possibile definire i metodi all'interno dell'interfaccia che eseguono query JDBC e gestire automaticamente tutti i dettagli intricati, ad esempio la definizione della semantica transazione corretta (transazioni di sola lettura per le query), l'esecuzione della query e il mapping del set di risultati alla classe entità Owner definita in precedenza.

    Il metodo findByName definito in precedenza genererà una query come SELECT ID, NAME, AGE FROM OWNER WHERE NAME = ? automaticamente al momento della compilazione.

    Per ulteriori informazioni sui metodi di query e sui tipi di query che è possibile definire, vedere la documentazione per i metodi di query nella documentazione sui dati Micronaut.

  3. In OwnerRepository definire un altro repository e questa volta utilizzando un oggetto di trasferimento dati (DTO) per eseguire una query ottimizzata. Per prima cosa, è necessario creare la classe DTO in un file denominato NameDTO.java in src/main/java/com/example/repositories:

    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;
        }
    }
    

    Un DTO è un POJO semplice che consente di selezionare solo le colonne necessarie per una determinata query, producendo una query più ottimizzata.

  4. Definire il repository denominato PetRepository in un file denominato PetRepository.java per l'entità Pet che utilizza il DTO nella stessa posizione 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);
    }
    

    Prendere nota del metodo list che restituisce il DTO. Questo metodo verrà di nuovo implementato in fase di compilazione, ma al posto di recuperare tutte le colonne della tabella Pet, recupererà solo la colonna name e qualsiasi altra colonna che è possibile definire.

    L'annotazione @Join eseguirà una query e creerà un'istanza dell'oggetto unito (Owner) e l'assegnerà al campo Owner dell'interrogazione Pet.

    Anche il metodo findByName è interessante in quanto utilizza un'altra caratteristica importante dei dati Micronaut, ovvero l'annotazione @Join. Consente di specificare percorsi di join in modo da recuperare esattamente i dati necessari tramite join di database, ottenendo query molto più efficienti.

Con i repository di dati esistenti, puoi passare all'esposizione degli endpoint REST.

Passo 3: espone i controller Micronaut come endpoint REST

Gli endpoint REST presenti in Micronaut sono facili da scrivere e vengono definiti come controller (in base al pattern MVC).

  1. Creare una cartella controllers in src/main/java/com/example/.

  2. Definire una nuova classe OwnerController in un file denominato 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);
        }
    }
    

    Una classe di controller è definita con l'annotazione @Controller che è possibile utilizzare per definire l'URI root a cui il controller esegue il mapping (in questo caso /owners).

    Notare l'annotazione @ExecuteOn utilizzata per indicare a Micronaut che il controller esegue la comunicazione I/O con un database e, di conseguenza, le operazioni devono eseguire il pool di thread I/O.

    La classe OwnerController utilizza l'iniezione di dipendenza Autonomo per ottenere un riferimento all'interfaccia repository OwnerRepository definita in precedenza e utilizzata per implementare due endpoint:

    • /: l'endpoint root elenca tutti i proprietari
    • /{name}: il secondo endpoint utilizza un modello URI per consentire la ricerca di un proprietario in base al nome. Il valore della variabile URI {name} viene fornito come parametro del metodo byName.
  3. Definire un altro endpoint REST denominato PetController in un file denominato PetController.java in 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);
        }
    }
    

    Questa volta viene iniettato il file PetRepository per esporre un elenco di animali domestici e animali domestici per nome.

Passo 4: popolamento dei dati all'avvio dell'applicazione

Il passo successivo consiste nel popolare alcuni dati dell'applicazione all'avvio. A tale scopo, è possibile utilizzare gli eventi dell'applicazione Micronaut.

Aprire la classe src/main/java/com/example/Application.java e sostituire il contenuto del file iniziale con quanto segue:

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));
    }
}

Tenere presente che il costruttore viene modificato per inserire le definizioni del repository in base alle dipendenze in modo da rendere persistenti i dati.

Infine, il metodo init viene annotato con @EventListener con un argomento per ricevere un StartupEvent. Questo evento viene chiamato quando l'applicazione è attiva e in esecuzione e può essere utilizzato per rendere persistenti i dati quando l'applicazione è pronta a farlo.

La parte restante dell'esempio mostra il salvataggio di alcune entità utilizzando il metodo saveAll dell'interfaccia CrudRepository.

Si noti che javax.transaction.Transactional è dichiarato nel metodo che garantisce che Micronaut Data avvolga l'esecuzione del metodo init in una transazione JDBC di cui viene eseguito il rollback se si verifica un'eccezione durante l'esecuzione del metodo.

Passo 5: esegue i test di integrazione per l'applicazione Micronaut

L'applicazione è già stata impostata con un singolo test che verifica se l'avvio dell'applicazione è riuscito, quindi eseguirà il test della logica del metodo init definito nella sezione precedente.

  1. Dalla navigazione in alto, passare a Terminale, quindi a Nuovo terminale.

  2. Eseguire l'obiettivo test per eseguire i test:

    ./mvnw test
    

    Vedere la struttura dei file ed eseguire i test in una finestra terminale in VS Code

    In alternativa, se si utilizza Gradle, eseguire il task test:

    ./gradlew test
    

    Al termine dell'esecuzione del test, vedere il messaggio BUILD SUCCESS.

È ora possibile passare al task successivo.

Per saperne di più