Nota:

TAREA 4: Implantación de consultas de base de datos y creación de una aplicación Micronaut

En este laboratorio, implantará consultas de base de datos y creará una aplicación Micronaut localmente que se conecte a Oracle Autonomous Database.

Tiempo estimado: 30 minutos

Contenido de tarea

En esta tarea:

Paso 1: Crear entidades de datos Micronaut que se asignan a tablas de Oracle Database

En la tarea anterior, ha agregado el script SQL que crearía una tabla denominada OWNER y una tabla denominada PET una vez ejecutada. A continuación, debe definir las clases de entidad que se pueden utilizar para leer datos de las tablas de la base de datos.

  1. Cree una clase de entidad que represente un Owner en el paquete src/main/java/com/example. Haga clic con el botón derecho en src/main/java/com/example para ampliar el menú de contenido, seleccione Nuevo archivo, asígnele el nombre Owner.java y pegue el siguiente código:

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

    La anotación @MappedEntity se utiliza para indicar que la entidad está asignada a una tabla de base de datos. Por defecto, será una tabla con el mismo nombre que la clase (en este caso owner).

    Las columnas de la tabla están representadas por cada propiedad de Java. En el caso anterior, se utilizará una columna id para representar la clave primaria y @GeneratedValue configurará la asignación para asumir el uso de una columna identity en Autonomous Database.

    La anotación @Creator se utiliza en el constructor que se utilizará para instanciar la entidad asignada y también se utiliza para expresar las columnas necesarias. En este caso, la columna name es necesaria e inmutable mientras que la columna age no es necesaria y se puede definir de forma independiente mediante el setter setAge.

  2. Cree un archivo Pet.java que represente la entidad Pet para modelar una tabla pet en 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
        }
    }
    

    Tenga en cuenta que la clase Pet utiliza una UUID rellenada automáticamente como clave primaria para demostrar diferentes enfoques en la generación de ID.

    Una relación entre la clase Pet y la clase Owner también se define mediante la anotación @Relation(Relation.Kind.MANY_TO_ONE), lo que indica que se trata de una relación de varios a uno.

    Con ello, es hora de pasar a definir interfaces de repositorio para implantar consultas.

Paso 2: Definición de repositorios de datos de Micronaut para implantar consultas

Micronaut Data soporta la noción de definición de interfaces que implantan automáticamente consultas SQL para el usuario en el momento de la compilación mediante el patrón del repositorio de datos. En esta sección, aprovechará esta función Micronaut Data.

  1. Cree una carpeta independiente denominada repositories en src/main/java/com/example.

  2. Defina una nueva interfaz de repositorio que se amplíe desde CrudRepository y se anote con @JdbcRepository mediante el dialecto ORACLE en un archivo denominado 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);
    }
    

    La interfaz CrudRepository toma 2 tipos de argumentos genéricos. El primero es el tipo de entidad (en este caso Owner) y el segundo es el tipo de ID (en este caso Long).

    La interfaz CrudRepository define métodos que permiten crear, leer, actualizar y suprimir entidades (CRUD) de la base de datos con las inserciones, selecciones, actualizaciones y supresiones SQL adecuadas calculadas en el momento de la compilación. Para obtener más información, consulte el Javadoc de CrudRepository.

    Puede definir métodos dentro de la interfaz que realizan consultas JDBC y manejan automáticamente todos los detalles intrincados, como la definición de semántica de transacción correcta (transacciones de sólo lectura para consultas), la ejecución de la consulta y la asignación del juego de resultados a la clase de entidad Owner definida anteriormente.

    El método findByName definido anteriormente producirá una consulta como SELECT ID, NAME, AGE FROM OWNER WHERE NAME = ? automáticamente en el momento de la compilación.

    Para obtener más información sobre los métodos de consulta y los tipos de consultas que puede definir, consulte la documentación de los métodos de consulta en la documentación de datos de Micronaut.

  3. Con OwnerRepository en su lugar, defina otro repositorio y esta vez mediante un objeto de transferencia de datos (DTO) para realizar una consulta optimizada. Por lo tanto, primero debe crear la clase DTO en un archivo denominado NameDTO.java en 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 es un POJO simple que permite seleccionar solo las columnas que necesita una consulta concreta, produciendo así una consulta más optimizada.

  4. Defina el repositorio denominado PetRepository en un archivo denominado PetRepository.java para la entidad Pet que utiliza el DTO en la misma ubicación 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);
    }
    

    Tome nota del método list que devuelve el DTO. Este método se volverá a implantar en el momento de la compilación, pero esta vez en lugar de recuperar todas las columnas de la tabla Pet, solo recuperará la columna name y cualquier otra columna que pueda definir.

    La anotación @Join consultará e instanciará el objeto unido (Owner) y lo asignará al campo Owner de Pet consultado.

    El método findByName también es interesante, ya que utiliza otra característica importante de Micronaut Data, que es la anotación @Join. Permite especificar rutas de acceso de unión para recuperar exactamente los datos que necesita mediante uniones de base de datos, lo que da como resultado consultas mucho más eficientes.

Con los repositorios de datos en su lugar, continúe con la exposición de puntos finales de REST.

Paso 3: Exponer los controladores de Micronaut como puntos finales de REST

Los puntos finales de REST en Micronaut son fáciles de escribir y se definen como controllers (según el patrón MVC).

  1. Cree una carpeta controllers en src/main/java/com/example/.

  2. Defina una nueva clase OwnerController en un archivo denominado 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 clase de controlador se define con la anotación @Controller que puede utilizar para definir el URI raíz al que se asigna el controlador (en este caso /owners).

    Observe la anotación @ExecuteOn que se utiliza para indicar a Micronaut que el controlador realiza la comunicación de E/S con una base de datos y, por lo tanto, las operaciones deben ejecutar en el pool de threads de E/S.

    La clase OwnerController utiliza la inyección de dependencia de micras para obtener una referencia a la interfaz de repositorio OwnerRepository definida anteriormente y se utiliza para implementar dos puntos finales:

    • /: el punto final raíz muestra todos los propietarios
    • /{name}: el segundo punto final utiliza una plantilla de URI para permitir buscar un propietario por nombre. El valor de la variable de URI {name} se proporciona como parámetro al método byName.
  3. A continuación, defina otro punto final de REST denominado PetController en un archivo denominado PetController.java en 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);
        }
    }
    

    Esta vez se inyecta PetRepository para exponer una lista de mascotas y mascotas por nombre.

Paso 4: Rellenar datos en el inicio de la aplicación

El siguiente paso es rellenar algunos datos de la aplicación al iniciar. Para ello, puede utilizar Micronaut application events.

Abra la clase src/main/java/com/example/Application.java y sustituya el contenido inicial del archivo por lo siguiente:

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

Tenga en cuenta que el constructor se modifica para que la dependencia inyecte las definiciones del repositorio para que se puedan mantener los datos.

Por último, el método init se anota con @EventListener con un argumento para recibir StartupEvent. Este evento se llama una vez que la aplicación está activa y en ejecución, y se puede utilizar para mantener los datos cuando la aplicación esté lista para hacerlo.

En el resto del ejemplo, se muestra cómo guardar algunas entidades con el método saveAll de la interfaz CrudRepository.

Observe que javax.transaction.Transactional se declara en el método que garantiza que Micronaut Data encapsula la ejecución del método init en una transacción JDBC de la que se realiza un rollback si se produce una excepción durante la ejecución del método.

Paso 5: Ejecución de pruebas de integración para la aplicación Micronaut

La aplicación ya se ha configurado con una única prueba que comprueba si la aplicación se puede iniciar correctamente (y, por lo tanto, probará la lógica del método init definido en la sección anterior).

  1. Desde la navegación superior, vaya a Terminal y, a continuación, a New Terminal.

  2. Ejecute el objetivo test para ejecutar pruebas:

    ./mvnw test
    

    Ver la estructura de archivos y ejecutar pruebas en una ventana de terminal en VS Code

    También puede ejecutar la tarea test si utiliza Gradle:

    ./gradlew test
    

    Debe ver el mensaje BUILD SUCCESS al final de la ejecución de la prueba.

Ahora puede continuar con la siguiente tarea.

Más información