注:

TASK 4:实施数据库查询并构建 Micronaut 应用程序

在此实验室中,您将实施数据库查询并在本地构建连接到 Oracle Autonomous Database 的 Micronaut 应用。

估计时间:30 分钟

任务内容

在本任务中,您将执行以下操作:

步骤 1:创建映射到 Oracle Database 表的 Micronaut 数据实体

在前面的任务中,您添加了 SQL 脚本,该脚本在执行后将创建一个名为 OWNER 的表和一个名为 PET 的表。接下来,您必须定义可用于从数据库表中读取数据的实体类。

  1. src/main/java/com/example 程序包中创建将表示 Owner 的实体类。右键单击 src/main/java/com/example 以展开内容菜单,选择“New File(新建文件)”,将其命名为 Owner.java,然后粘贴以下代码:

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

    @MappedEntity 注释用于指示实体已映射到数据库表。默认情况下,此表将与类同名(在本例中为 owner)。

    表中的列由每个 Java 属性表示。在以上情况下,id 列将用于表示主键,@GeneratedValue 将设置映射以假定在 Autonomous Database 中使用 identity 列。

    @Creator 注释在用于实例化映射实体的构造器上使用,还用于表示必需列。在这种情况下,name 列是必需的且不可变的,而 age 列不是必需的,可以使用 setAge setter 独立设置。

  2. 创建一个 Pet.java 文件,表示 Pet 实体,以对 src/main/java/com/example 下的 pet 表进行建模:

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

    请注意,Pet 类使用自动填充的 UUID 作为主键来演示 ID 生成的不同方法。

    Pet 类与 Owner 类之间的关系也使用 @Relation(Relation.Kind.MANY_TO_ONE) 注释进行定义,这表示这是多对一关系。

    完成此操作后,现在是进入定义系统信息库接口以实施查询的时候了。

步骤 2:定义用于实施查询的 Micronaut 数据资料档案库

Micronaut 数据支持定义接口的概念,这些接口使用数据存储库模式在编译时自动为您实施 SQL 查询。在此部分中,您将利用此微宇航员数据功能。

  1. src/main/java/com/example 下创建一个名为 repositories 的单独文件夹。

  2. 使用名为 OwnerRepository.java 的文件中的 ORACLE 方言定义从 CrudRepository 扩展且使用 @JdbcRepository 进行注释的新系统信息库接口:

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

    CrudRepository 接口接受 2 个通用参数类型。第一个是实体的类型(在本例中为 Owner),第二个是 ID 的类型(在本例中为 Long)。

    CrudRepository 界面定义了允许您使用编译时为您计算的相应 SQL 插入、选择、更新和删除 (CRUD) 实体在数据库中创建、读取、更新和删除 (CRUD) 实体的方法。有关更多信息,请访问 CrudRepository 的 Javadoc。

    您可以在界面中定义执行 JDBC 查询的方法并自动处理您的所有复杂详细信息,例如定义正确的事务处理语义(查询的只读事务处理),执行查询并将结果集映射到之前定义的 Owner 实体类。

    上面定义的 findByName 方法将在编译时自动生成诸如 SELECT ID, NAME, AGE FROM OWNER WHERE NAME = ? 之类的查询。

    有关查询方法和可以定义的查询类型的更多信息,请参阅 Micronaut 数据文档中的查询方法的文档

  3. OwnerRepository 就位的情况下,定义另一个系统信息库,这次使用数据传输对象 (data transfer object, DTO) 执行优化查询。因此,首先需要在 src/main/java/com/example/repositories 下名为 NameDTO.java 的文件中创建 DTO 类:

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

    DTO 是一个简单的 POJO,允许您仅选择特定查询需要的列,从而生成更优化的查询。

  4. 对于在同一位置 src/main/java/com/example/repositories 中使用 DTO 的 Pet 实体,在名为 PetRepository.java 的文件中定义名为 PetRepository 的系统信息库:

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

    请注意返回 DTO 的 list 方法。将在编译时再次为您实施此方法,但这次,它不会检索 Pet 表的所有列,而是仅检索 name 列和您可能定义的任何其他列。

    @Join 注释将查询并实例化联接的对象 (Owner),并将其分配给查询的 PetOwner 字段。

    findByName 方法也很有趣,因为它使用 @Join 注释的 Micronaut 数据的另一个重要功能。它允许您指定联接路径,以便通过数据库联接准确检索所需的数据,从而提高查询效率。

在部署数据存储库后,继续公开 REST 端点。

步骤 3:将 Micronaut 控制器公开为 REST 端点

Micronaut 中的 REST 端点易于编写,定义为控制器(根据 MVC 模式)

  1. src/main/java/com/example/ 下创建一个文件夹 controllers

  2. 在名为 OwnerController.java 的文件中定义新的 OwnerController 类:

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

    控制器类是使用 @Controller 注释定义的,可用于定义控制器映射到的根 URI(在本例中为 /owners)。

    请注意 @ExecuteOn 注释,该注释用于告知 Micronaut 控制器与数据库执行 I/O 通信,因此,操作在 I/O 线程池上运行

    OwnerController 类使用 Micronaut 依赖性注入获取对之前定义的 OwnerRepository 系统信息库接口的引用,用于实现两个端点:

    • / - 根端点列出了所有所有者
    • /{name}- 第二个端点使用 URI 模板允许按名称查找所有者。URI 变量 {name} 的值作为 byName 方法的参数提供。
  3. 接下来,在 src/main/java/com/example/controllers 下名为 PetController.java 的文件中定义名为 PetController 的另一个 REST 端点:

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

    这次注入 PetRepository 以按名称显示 pets 和 pets 的列表。

步骤 4:在应用程序启动时填充数据

下一步是在启动时填充某些应用程序数据。为此,可以使用 Micronaut 应用程序事件

打开 src/main/java/com/example/Application.java 类,并将初始文件内容替换为以下内容:

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

请注意,构造器已修改为依赖项注入系统信息库定义,因此可以持久保存数据。

最后,init 方法使用带有参数的 @EventListener 进行注释,以接收 StartupEvent。此事件在应用程序启动并运行后立即调用,可在应用准备就绪时用来保留数据。

本示例的其余部分演示如何使用 CrudRepository 接口的 saveAll 方法保存一些实体。

请注意,在该方法上声明了 javax.transaction.Transactional,该方法可确保 Micronaut 数据在 JDBC 事务处理中包装 init 方法的执行,该方法在执行方法期间发生异常时回退。

步骤 5:为 Micronaut 应用程序运行集成测试

已使用单个测试设置应用程序,该测试检查应用程序是否可以成功启动(因此将测试上一节中定义的 init 方法的逻辑)。

  1. 从顶部导航中,依次转至 Terminal(终端)New Terminal(新建终端)

  2. 运行 test 目标以执行测试:

    ./mvnw test
    

    在 VS Code 的终端窗口中查看文件结构并执行测试

    或者,如果您使用 Gradle,请运行 test 任务:

    ./gradlew test
    

    测试运行结束时应看到 BUILD SUCCESS 消息。

您现在可以继续执行下一个任务

了解更多