vlambda博客
学习文章列表

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用Quarkus管理数据持久性

Managing Data Persistence with Quarkus

到目前为止,我们已经使用可通过 REST 通道访问的内存结构开发了一些基本应用程序。但这仅仅是开始。在现实世界的示例中,您不仅仅依赖于内存中的数据;相反,您将数据结构保存在关系数据库或其他地方,例如 NoSQL 存储中。因此,在本章中,我们将利用所需的基本技能在 Quarkus 中构建将数据持久保存到关系数据库中的应用程序。我们还将学习如何使用 对象关系映射 (ORM) 工具(例如 Hibernate ORM)将数据库映射为存储,以及如何使用 简化其使用带有 Panache 扩展的休眠 ORM。

在本章中,我们将介绍以下主题:

  • Adding an ORM layer to the customer service
  • Configuring and running an application to reach an RDBMS
  • Taking both services (application and database) into the cloud
  • Adding Hibernate ORM with Panache on top of your application to simplify the ORM

Adding an ORM layer to our applications

如果您以前从事过企业项目,您就会知道几乎每个 Java 应用程序都使用 ORM 工具来映射外部数据库。使用 Java 对象映射数据库结构的优点如下:

  • Database neutrality: Your code will not be database-specific, so you don't need to adapt your code to a specific database SQL syntax, which may vary between vendors.
  • Developer friendly workflow: You don't need to write complex SQL structures to access your data – you simply need to refer to Java fields.

另一方面,通过编写本机 SQL 语句,您可以真正了解您的代码实际在做什么,这也是事实。此外,在大多数情况下,您可以通过编写直接 SQL 语句来获得最大的性能优势。出于这个原因,大多数 ORM 工具都包含一个执行本机 SQL 语句以绕过标准 ORM 逻辑的选项。

在 Quarkus 工具包中,您可以使用 quarkus-hibernate-orm 扩展将您的 Java 类映射为实体对象。 Hibernate ORM 位于 Java 应用程序数据访问层和关系数据库之间。您可以使用 Hibernate ORM API 来执行查询、删除、存储和域数据等操作。

首先,让我们为我们的应用程序定义领域模型。我们将从简单的 Customer 对象开始,因为我们已经知道它是什么。为了使我们的示例更有趣,我们将添加另一个对象,称为 Orders,它与我们的 Customer 对象相关。准确地说,我们将在 Customer 与其 Orders 之间声明 一对多 关系:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用Quarkus管理数据持久性

首先,让我们查看本章的第一个示例,它位于本书 GitHub 存储库的 Chapter05/hibernate 文件夹中。我们建议在继续之前将项目导入您的 IDE。

如果你检查这个项目的 pom.xml 文件,你会发现其中包含几个新的扩展:

  • quarkus-hibernate-orm: This extension is the core dependency that we need in order to use Hibernate's ORM tool in our application.
  • quarkus-agroal: This extension buys us the Agroal connection pool, which will handle JDBC connection management for us.
  • quarkus-jdbc-postgresql: This extension contains the JDBC modules that we need in order to connect to the PostgreSQL database.
  • quarkus-resteasy-jsonb: This extension is needed so that we can create JSON items at runtime and produce a JSON response.

以下代码将附加依赖项显示为 XML 元素:

<dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-agroal</artifactId>
</dependency>
<dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>

现在我们已经查看了项目的配置,让我们检查构成我们应用程序的单个组件。

Defining the entity layer

我们需要检查的第一件事是将映射数据库表的实体对象列表。第一个是 Customer @Entity 类,如下:

@Entity
@NamedQuery(name = "Customers.findAll",
        query = "SELECT c FROM Customer c ORDER BY c.id",
        hints = @QueryHint(name = "org.hibernate.cacheable", value = 
        "true") )
public class Customer {
    @Id
    @SequenceGenerator(
            name = "customerSequence",
            sequenceName = "customerId_seq",
            allocationSize = 1,
            initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 
     "customerSequence")
    private Long id;

    @Column(length = 40)
    private String name;

    @Column(length = 40)
    private String surname;

    @OneToMany(mappedBy = "customer")
    @JsonbTransient
    public List<Orders> orders;

 // Getters / Setters omitted for brevity
}

让我们来看看我们在实体类中包含的单个注释:

  • The @Entity annotation makes this class eligible for persistence. It can be coupled with the @Table annotation to define the corresponding database table to a map. If it's not included, like in our case, it will map a database table with the same name.
  • The @NamedQuery annotation (placed at the class level) is a statically defined SQL statement featuring a query string. Using named queries in your code improves how your code is organized since it separates the JPA query language from the Java code. It also avoids the bad practice of embedding string literals directly in your SQL, thus enforcing the use of parameters instead.
  • The @Id annotation specifies the primary key of an entity, which will be unique for every record.
  • The @SequenceGenerator annotation is used to delegate the creation of a sequence as a unique identifier for primary keys. You will need to check that your database is capable of handling sequences. On the other hand, although this isn't the default option, this is considered a safer alternative since the identifier can be generated prior to executing the INSERT statement.
  • The @Column annotation is used to tell Hibernate ORM that the Java field maps a database column. Note that we have also specified a constraint in terms of the size of the column. Since we will let Hibernate ORM create our database structures from Java code, all the constraints that are declared in the Java class will effectively turn into database constraints.
  • Finally, we had to apply two annotations on top of the orders field:
    • The @OneToMany annotation defines a one-to-many relationship with the Orders table (that is, one customer is associated with many orders).
    • The @JsonbTransient annotation prevents mapping the field to the JSON representation (since the reverse mapping for this relationship is included in the Orders class, mapping this field to JSON would cause a StackOverflow error).
In our code example, we have omitted the getter/setter methods for the sake of brevity. These are, however, needed by Hibernate ORM to perform entity reads and writes against the database. In the Making data persistence easier with Hibernate Panache section later in this chapter, we will learn how to make our code leaner and cleaner by extending the PanacheEntity API.

Customer 实体又引用了以下 Orders 类,它提供了一对多注释的另一面:

@Entity
@NamedQuery(name = "Orders.findAll",
        query = "SELECT o FROM Orders o WHERE o.customer.id = :customerId ORDER BY o.item")
public class Orders {
    @Id
    @SequenceGenerator(
            name = "orderSequence",
            sequenceName = "orderId_seq",
            allocationSize = 1,
            initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 
     "orderSequence")
    public Long id;

    @Column(length = 40)
    public String item;

    @Column
    public Long price;

    @ManyToOne
    @JoinColumn(name = "customer_id") 
    @JsonbTransient
    public Customer customer;

 // Getters / Setters omitted for brevity
}

值得注意的是,这个类的命名查询稍微详细一些,因为 Orders.findAll NamedQuery 使用一个参数来过滤特定客户的订单。

由于Customer结构和Orders结构构成双向关联,我们需要将对应的Customer字段映射到@javax .persistence.ManyToOne 注释。

我们还包含了 @javax.persistence.JoinColumn 注释,以表明该实体是关系的所有者。在数据库术语中,这意味着对应的表有一个列,该列具有引用表的外键。现在我们有了一个将存储数据的类,让我们检查用于从 RDBMS 访问数据的 Repository 类。

Coding the repository classes

为了访问我们的 Customer 数据,我们仍然依赖于需要调整的 CustomerRepository 类。首先,我们注入了一个EntityManager接口的实例为了管理实体实例的持久性:

@ApplicationScoped
public class CustomerRepository {

    @Inject
    EntityManager entityManager;

}

一旦我们有了对 EntityManager 的引用,我们就可以使用它对类的其余部分执行 CRUD 操作:

public List<Customer> findAll() {
        return entityManager.createNamedQuery("Customers.findAll", 
         Customer.class)
                .getResultList();
}

public Customer findCustomerById(Long id) {
        Customer customer = entityManager.find(Customer.class, id);

        if (customer == null) {
            throw new WebApplicationException("Customer with id of " + 
             id + " does not exist.", 404);
        }
        return customer;
}

@Transactional
public void updateCustomer(Customer customer) {
        Customer customerToUpdate = findCustomerById(customer.
         getId());
        customerToUpdate.setName(customer.getName());
        customerToUpdate.setSurname(customer.getSurname());
}

@Transactional
public void createCustomer(Customer customer) {
        entityManager.persist(customer);
}

@Transactional
public void deleteCustomer(Long customerId) {
        Customer c = findCustomerById(customerId);
        entityManager.remove(c);
}

需要注意的是,我们已经用 @javax.transaction.Transactional 注释标记了所有执行写操作的方法。这是在 Quarkus 应用程序中划分事务边界的最简单方法,就像我们过去在 Java Enterprise 应用程序中所做的那样。在实践中,@Transactional 方法将在调用者事务的上下文中运行,如果有的话。否则,它将在运行该方法之前启动一个新事务。

接下来,我们创建了一个 Repository 类,该类也用于管理订单。 OrderRepository 类几乎等同于 CustomerRepository 类,除了 findAll 方法将过滤特定客户的订单:

@ApplicationScoped
public class OrderRepository {

    @Inject
    EntityManager entityManager;

    public List<Orders> findAll(Long customerId) {

      return  (List<Orders>) 
        entityManager.createNamedQuery("Orders.findAll")
                .setParameter("customerId", customerId)
                .getResultList();
    }

    public Orders findOrderById(Long id) {

        Orders order = entityManager.find(Orders.class, id);
        if (order == null) {
            throw new WebApplicationException("Order with id of " + id 
             + " does not exist.", 404);
        }
        return order;
    }
    @Transactional
    public void updateOrder(Orders order) {
        Orders orderToUpdate = findOrderById(order.getId());
        orderToUpdate.setItem(order.getItem());
        orderToUpdate.setPrice(order.getPrice());
    }
    @Transactional
    public void createOrder(Orders order, Customer c) {
        order.setCustomer(c);
        entityManager.persist(order);

    }
    @Transactional
    public void deleteOrder(Long orderId) {
        Orders o = findOrderById(orderId);
        entityManager.remove(o);
    }
}

现在我们已经讨论了 Repository 和实体类,让我们看看 REST 端点,它使应用程序响应。

Defining REST endpoints

我们的应用程序为每个 Repository 类定义了一个 REST 端点。我们已经在上一章中编写了 CustomerEndpoint 代码,它完全不知道它是否正在使用存储。因此,已经完成了一半的工作。我们只在此处添加了 OrderEndpoint,它相应地映射 CRUD HTTP 操作:

@Path("orders")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class OrderEndpoint {

    @Inject OrderRepository orderRepository;
    @Inject CustomerRepository customerRepository;

    @GET
    public List<Orders> getAll(@QueryParam("customerId") Long 
     customerId) {
        return orderRepository.findAll(customerId);
    }

    @POST
    @Path("/{customer}")
    public Response create(Orders order, @PathParam("customer") Long 
     customerId) {
        Customer c = customerRepository.findCustomerById(customerId);
        orderRepository.createOrder(order,c);
        return Response.status(201).build();

    }

    @PUT
    public Response update(Orders order) {
        orderRepository.updateOrder(order);
        return Response.status(204).build();
    }
    @DELETE
    @Path("/{order}")
    public Response delete(@PathParam("order") Long orderId) {
        orderRepository.deleteOrder(orderId);
        return Response.status(204).build();
    }

}

我们的 OrderEndpoint 稍微复杂一些,因为它需要通过 getAll 方法中的 Customer ID 过滤每个订单操作。我们还在代码中使用 @PathParam 注释将 CustomerOrders 数据从客户端移动到 REST 端点。

Connecting to the database

数据库连接是通过 Quarkus 的主配置文件 (application.properties) 建立的,它至少需要数据库的 JDBC 设置。我们将使用 PostgreSQL 作为存储,以便 JDBC URL 和驱动程序符合 PostgreSQL JDBC 的规范。以下配置将用于访问 quarkusdb 数据库,该数据库使用 quarkus/quarkus 凭据:

quarkus.datasource.url=jdbc:postgresql://${POSTGRESQL_SERVICE_HOST:localhost}:${POSTGRESQL_SERVICE_PORT:5432}/quarkusdb
quarkus.datasource.driver=org.postgresql.Driver
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
Note that we are using two environment variables ( POSTGRESQL_SERVICE_HOST and POSTGRESQL_SERVICE_PORT) to define the database host and port. If they're left undefined, they will be set to localhost and 5432. This configuration will come in handy when we switch our application from the local filesystem to the cloud.

接下来,我们将 Hibernate ORM 配置为在启动时使用 drop and create 策略。这是开发或测试应用程序的理想选择,因为每次我们启动应用程序时,它都会从 Java 实体中删除并重新生成模式和数据库对象:

quarkus.hibernate-orm.database.generation=drop-and-create

此外,我们还包括了 Agroal 连接池设置来定义池的初始大小、内存中保持可用的最小连接数以及可以打开的最大同时连接数:

quarkus.datasource.initial-size=1
quarkus.datasource.min-size=2
quarkus.datasource.max-size=8

最后,为了测试目的,在我们的模式中预先插入一些行会很有用。因此,我们使用以下属性设置了脚本 (import.sql) 所在的位置:

quarkus.hibernate-orm.sql-load-script=import.sql

import.sql 脚本中的以下内容将两行添加到 Customer 表中:

INSERT INTO customer (id, name, surname) VALUES ( nextval('customerId_seq'), 'John','Doe');
INSERT INTO customer (id, name, surname) VALUES ( nextval('customerId_seq'), 'Fred','Smith');

上述 SQL 脚本可以在 src/main/resources 文件夹中找到。

现在我们已经检查了我们的服务,我们将检查测试类,它会自动验证 CRUD 操作。然后,我们将看一下 Web 界面,以便我们可以通过浏览器测试代码。

Coding a test class

我们的基本 Test 类假定我们已经有几个可用的 Customer 对象。因此,一旦我们通过 GET 请求验证了它们的计数,我们将测试 Orders 子实体上的所有 CRUD 操作,如下面的代码所示:

// Test GET
given()
        .when().get("/customers")
        .then()
        .statusCode(200)
        .body("$.size()", is(2));

// Create a JSON Object for the Order
JsonObject objOrder = Json.createObjectBuilder()
        .add("item", "bike")
        .add("price", new Long(100))
        .build();


// Test POST Order for Customer #1
given()
        .contentType("application/json")
        .body(objOrder.toString())
        .when()
        .post("/orders/1")
        .then()
        .statusCode(201);

// Create new JSON for Order #1
objOrder = Json.createObjectBuilder()
        .add("id", new Long(1))
        .add("item", "mountain bike")
        .add("price", new Long(100))
        .build();

// Test UPDATE Order #1
given()
        .contentType("application/json")
        .body(objOrder.toString())
        .when()
        .put("/orders")
        .then()
        .statusCode(204);

// Test GET for Order #1
given()
        .when().get("/orders?customerId=1")
        .then()
        .statusCode(200)
        .body(containsString("mountain bike"));

// Test DELETE Order #1
given()
        .when().delete("/orders/1")
        .then()
        .statusCode(204);

在这一点上,这个测试类不应该太复杂。我们基本上是在使用 org.hamcrest.CoreMatchers.is 构造测试这两个客户在数据库中是否可用。然后,我们对 Orders 实体执行一轮完整的操作,创建一个项目,更新它,查询它,最后删除它。

在运行测试之前,我们需要一个可用的数据库来保存数据。如果您没有活动的 PostgreSQL 实例,推荐的方法是使用以下 shell 启动 docker 映像:

$ docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_test -e POSTGRES_USER=quarkus -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=quarkusdb -p 5432:5432 postgres:10.5

请注意,除了数据库用户、密码和数据库设置之外,我们还通过 --ulimit memlock=-1:-1 设置强制执行我们的容器,以便对防止交换。我们还将数据库的地址和端口转发到本地计算机上可用的所有 IPv4/IPv6 地址。

docker 进程启动时将发出以下输出:

2019-07-09 14:05:56.235 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2019-07-09 14:05:56.235 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2019-07-09 14:05:56.333 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-07-09 14:05:56.434 UTC [60] LOG:  database system was shut down at 2019-07-09 14:05:56 UTC
2019-07-09 14:05:56.516 UTC [1] LOG:  database system is ready to accept connections

现在,您可以使用以下命令启动测试类:

$ mvn compile test

预期输出应确认测试成功运行:

[INFO] Running com.packt.quarkus.chapter5.CustomerEndpointTest
. . . .
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 11.846 s - in com.packt.quarkus.chapter5.CustomerEndpointTest

现在,我们将检查我们添加的项目的静态网页,以便我们可以访问和管理我们的服务。

Adding a web interface to our application

我们的服务包括两个静态网页来管理客户服务和每个客户的订单。正如您从上一章中知道的那样,默认情况下,静态页面位于项目的 src/main/resources/META-INF/resources 文件中。我们可以重复使用上一章中的 index.html 页面,这将是我们应用程序的登录页面。但是,您会发现一个增强功能是一个名为 Add Order 的操作,它将我们的用户重定向到 order.html 页面,该页面传递了一个查询参数客户信息:

<div class="divTable blueTable">
    <div class="divTableHeading">
        <div class="divTableHead">Customer Name</div>
        <div class="divTableHead">Customer Address</div>
        <div class="divTableHead">Action</div>
    </div>
    <div class="divTableRow" ng-repeat="customer in customers">
        <div class="divTableCell">{{ customer.name }}</div>
        <div class="divTableCell">{{ customer.surname }}</div>
        <div class="divTableCell">
            <a ng-href="/order.html?customerId={{ customer.id }}&customerName={{ customer.name }}& customerSurname={{ customer.surname }}" class="myButton">Orders</a>
            <a ng-click="edit( customer )" class="myButton">Edit</a>
            <a ng-click="remove( customer )" class="myButton">Remove</a>
        </div>
    </div>
</div>

order.html 页面有它自己的 AngularJS 控制器,它负责为选中的 Customer 显示一组 Orders,让我们可以阅读、创建、修改或删除现有订单。以下是 Angular Controller 的第一部分,定义了模块和控制器名称,并收集了表单参数:

var app = angular.module("orderManagement", []);
angular.module('orderManagement').constant('SERVER_URL', '/orders');

//Controller Part
app.controller("orderManagementController", function($scope, $http, SERVER_URL) {

 var customerId = getParameterByName('customerId');
 var customerName = getParameterByName('customerName');
 var customerSurname = getParameterByName('customerSurname');

 document.getElementById("info").innerHTML = customerName + " " + customerSurname;

 $scope.orders = [];

 $scope.form = {
 customerId: customerId,
 isNew: true,
 item: "",
 price: 0
 };
 //Now load the data from server
 reloadData();

在 JavaScript 代码的第二部分,我们包含了一个 $scope.update 函数来插入/编辑新的 Orders,一个 $scope.remove 函数删除现有订单,以及一个 reloadData 函数检索该 CustomerOrders 列表,如以下代码所示:

 //HTTP POST/PUT methods for add/edit orders
 $scope.update = function() {

 var method = "";
 var url = "";
 var data = {};
 if ($scope.form.isNew == true) {
 // add orders - POST operation
 method = "POST";
 url = SERVER_URL + "/" + customerId;
 data.item = $scope.form.item;
 data.price = $scope.form.price;

 } else {
 // it's edit operation - PUT operation
 method = "PUT";
 url = SERVER_URL;

 data.item = $scope.form.item;
 data.price = $scope.form.price;

 }

 if (isNaN(data.price)) {
 alert('Price must be a Number!');
 return false;
 }

 $http({
 method: method,
 url: url,
 data: angular.toJson(data),
 headers: {
 'Content-Type': 'application/json'
 }
 }).then(_success, _error);
 };


 //HTTP DELETE- delete order by id
 $scope.remove = function(order) {
 $http({
 method: 'DELETE',
 url: SERVER_URL + "/" + order.id
 }).then(_success, _error);
 };

 //In case of edit orders, populate form with order data
 $scope.edit = function(order) {
 $scope.form.item = order.item;
 $scope.form.price = order.price;
 $scope.form.isNew = false;
 };
 /* Private Methods */
 //HTTP GET- get all orders collection
 function reloadData() {
 $http({
 method: 'GET',
 url: SERVER_URL,
 params: {
 customerId: customerId
 }
 }).then(function successCallback(response) {
 $scope.orders = response.data;
 }, function errorCallback(response) {
 console.log(response.statusText);
 });
 }

 function _success(response) {
 reloadData();
 clearForm()
 }

 function _error(response) {
 alert(response.data.message || response.statusText);
 }
 //Clear the form
 function clearForm() {
 $scope.form.item = "";
 $scope.form.price = "";
 $scope.form.isNew = true;
 }
});

为简洁起见,我们没有包含完整的 HTML 页面,但您可以在本书的 GitHub 存储库中找到它(正如我们在本章开头的技术要求部分中提到的)。

Running the application

应用程序可以从我们运行测试的同一个 shell 中执行(这样我们仍然保留 DB_HOST 环境变量):

mvn quarkus:dev

您应该在控制台中看到以下输出:

Listening for transport dt_socket at address: 5005
2019-07-14 18:41:32,974 INFO  [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
2019-07-14 18:41:33,789 INFO  [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 815ms
2019-07-14 18:41:35,153 INFO  [io.quarkus] (main) Quarkus 0.19.0 started in 2.369s. Listening on: http://[::]:8080
2019-07-14 18:41:35,154 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-postgresql, narayana-jta, resteasy, resteasy-jsonb]

现在,使用以下 URL 转到登录页面:http://localhost:8080。在这里,您将看到一个预先填写的客户列表:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用Quarkus管理数据持久性

尝试通过单击 Orders 按钮为客户添加一些订单。您将被带到以下 UI,您可以在其中读取、修改、删除和存储每个客户的新订单:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用Quarkus管理数据持久性

伟大的!该应用程序按预期工作。可以进一步改进吗?从性能的角度来看,如果我们缓存经常访问的数据,可以提高应用程序的吞吐量。下一节将向您展示如何使用 Hibernate ORM 的缓存机制来完成此操作。

Caching entity data

通过其高级缓存机制,可以在 Hibernate ORM 中轻松配置缓存实体。开箱即用的三种缓存:

  • The first-level cache is a transaction-level cache that's used to track the state of the entities during the current session. It's enabled by default.
  • The second-level cache is used to cache entities across various Hibernate ORM sessions. This makes it a SessionFactory-level cache.
  • The query cache is used to cache Hibernate ORM queries and their results.

二级缓存和查询缓存默认不启用,因为它们可能会消耗大量内存。要使实体有资格缓存其数据,您可以使用 @javax.persistence.Cacheable 注释对其进行注释,如以下代码所示:

@Cacheable
@Entity
public class Customer {

}

在这种情况下,客户的字段值被缓存,除了集合和与其他实体的关系。这意味着实体一旦被缓存,就可以通过其主键进行搜索,而无需查询数据库。

HQL 查询的结果也可以被缓存。当您想要对以读取为主的实体对象执行查询时,这可能非常有用。使 HQL 查询可缓存的最简单方法是将 @javax.persistence.QueryHint 注释添加到 @NamedQuery,并带有 org.hibernate.cacheable 属性设置为true,如下:

@Cacheable
@Entity
@NamedQuery(name = "Customers.findAll",
        query = "SELECT c FROM Customer c ORDER BY c.id",
        hints = @QueryHint(name = "org.hibernate.cacheable", value = 
        "true") )
public class Customer {   
}

您可以通过在 application.properties 文件中打开 SQL 日志记录来轻松验证上述断言,如下所示:

quarkus.hibernate-orm.log.sql=true

然后,如果您运行该应用程序,您应该能够看到一个 single SQL 语句,您可以使用该语句在控制台中查询 Customer 列表,无论您执行多少次已请求页面:

Hibernate: 
    select
        customer0_.id as id1_0_,
        customer0_.name as name2_0_,
        customer0_.surname as surname3_0_ 
    from
        Customer customer0_ 
    order by
        customer0_.id

伟大的!您已经达到了第一个里程碑,即在本地文件系统上运行应用程序并将常用的 SQL 语句缓存在 Hibernate ORM 的 二级缓存 (2LC) 中。现在,是时候将我们的应用程序带到云端了!

Taking an application to the cloud

通过本地 JVM 测试了应用程序之后,是时候将其本地引入云中了。这个过程的有趣部分是将 Quarkus 应用程序与 OpenShift 上的 PostgreSQL 应用程序连接起来,而无需触及任何一行代码!让我们看看我们如何实现这一点:

  1. Start your Minishift instance and create a new project named quarkus-hibernate:
oc new-project quarkus-hibernate
  1. Next, we will be adding a PostgreSQL application to our project. A PostgreSQL image stream is included in the openshift namespace by default, which you can check with the following command:
oc get is -n openshift | grep postgresql

您应该在控制台中看到以下输出:

postgresql   172.30.1.1:5000/openshift/postgresql   latest,10,9.2 + 3 more...    6 hours ago

要创建 PostgreSQL 应用程序,需要设置以下配置变量:

  • POSTGRESQL_USER: Username for the PostgreSQL account to be created
  • POSTGRESQL_PASSWORD: Password for the user account
  • POSTGRESQL_DATABASE: Database name

我们将使用我们在 application.properties 文件中定义的相同参数,以便我们可以使用以下命令引导我们的应用程序:

oc new-app -e POSTGRESQL_USER=quarkus -e POSTGRESQL_PASSWORD=quarkus -e POSTGRESQL_DATABASE=quarkusdb postgresql

在您的控制台日志中,检查是否已生成以下输出:

--> Creating resources ...
     imagestreamtag.image.openshift.io "postgresql:10" created
     deploymentconfig.apps.openshift.io "postgresql" created
     service "postgresql" created
 --> Success
     Application is not exposed. You can expose services to the 
    outside world by executing one or more of the commands below:
      'oc expose svc/postgresql'
     Run 'oc status' to view your app.

我们看一下可用服务列表(oc get services)来验证postgresql是否可用:

NAME                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
postgresql          ClusterIP   172.30.154.130   <none>        5432/TCP   14m

如您所见,该服务现在在集群 IP 地址 172.30.154.130 上处于活动状态。幸运的是,我们不需要在应用程序代码中对该地址进行硬编码,因为我们将使用服务名称 postgresql,它的工作方式类似于集群地址的别名。

现在,我们将创建项目的二进制构建,以便可以将其部署到 Minishift 中。不耐烦的用户只需执行 deploy-openshift.sh 脚本,该脚本可在 GitHub 上的本章根文件夹中找到。在其中,您将找到以下带注释的命令列表:

# Build native application
mvn package -Pnative -Dnative-image.docker-build=true -DskipTests=true

# Create a new Binary Build named "quarkus-hibernate"
oc new-build --binary --name=quarkus-hibernate -l app=quarkus-hibernate

# Set the dockerfilePath attribute into the Build Configuration
oc patch bc/quarkus-hibernate -p '{"spec":{"strategy":{"dockerStrategy":{"dockerfilePath":"src/main/docker/Dockerfile.native"}}}}'

# Start the build, uploading content from the local folder: 
oc start-build quarkus-hibernate --from-dir=. --follow

# Create a new Application, using as Input the "quarkus-hibernate" image stream:
oc new-app --image-stream=quarkus-hibernate:latest

# Expose the Service through a Route:
oc expose svc/quarkus-hibernate

在此过程结束时,您应该能够通过 oc get routes 命令看到以下可用路由:

NAME                HOST/PORT                                                  PATH      SERVICES            PORT       TERMINATION   WILDCARD
quarkus-hibernate   quarkus-hibernate-quarkus-hibernate.192.168.42.30.nip.io             quarkus-hibernate   8080-tcp                 None

还可以从项目的 Web 控制台检查应用程序的整体状态:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》使用Quarkus管理数据持久性

您现在可以导航到应用程序的外部路由(实际路由地址会有所不同,具体取决于您的网络配置。在我们的示例中,它是 http://quarkus-hibernate-quarkus-hibernate。 192.168.42.30.nip.io) 并检查应用程序在云上是否正常运行。

Making data persistence easier using Panache API

Hibernate ORM 是将数据库结构映射到 Java 对象的标准方法。使用 ORM 工具的主要缺点是,即使是简单的数据库结构也需要大量样板代码(例如 getter 和 setter 方法)。此外,您必须在存储库类中包含基本查询方法,这使得工作非常重复。在本节中,我们将学习如何使用 Hibernate Panache 来简化和加速我们的应用程序的开发。

要开始使用带有 Panache 的 Hibernate ORM,让我们查看本章的第二个示例,它位于本书 GitHub 存储库的 Chapter05/hibernate-panache 文件夹中。我们建议您在继续之前将项目导入您的 IDE。

如果你看一下项目的配置,你会看到我们在 pom.xml 文件中包含了 quarkus-hibernate-orm-panache

<dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>

这是我们使用 Hibernate Panache 所需的唯一配置。现在是有趣的部分。将 Panache 插入您的实体有两种策略:

  • Extending the io.quarkus.hibernate.orm.panache.PanacheEntity class: This is the simplest option as you will get an ID field that is auto-generated.
  • Extending io.quarkus.hibernate.orm.panache.PanacheEntityBase: This option can be used if you require a custom ID strategy.

由于我们对 ID 字段使用 SequenceGenerator 策略,我们将使用后一个选项。以下是 Customer 类,它已被重写,以便扩展 PanacheEntityBase

@Entity
@NamedQuery(name = "Customers.findAll",
         query = "SELECT c FROM Customer c ORDER BY c.id" )
public class Customer extends PanacheEntityBase {
     @Id
     @SequenceGenerator(
             name = "customerSequence",
             sequenceName = "customerId_seq",
             allocationSize = 1,
             initialValue = 1)
     @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 
      "customerSequence")
     public Long id;
 
     @Column(length = 40)
     public String name;

     @Column(length = 40)
     public String surname;

     @OneToMany(mappedBy = "customer")
     @JsonbTransient
     public List<Orders> orders;
}

如您所见,由于我们没有使用 getter/setter 字段,因此代码已经减少了很多。相反,某些字段已公开为 public 以便类可以直接访问它们。 Orders 实体已使用相同的模式重写:

@Entity
@NamedQuery(name = "Orders.findAll",
         query = "SELECT o FROM Orders o WHERE o.customer.id = :id ORDER BY o.item")
public class Orders extends PanacheEntityBase {
     @Id
     @SequenceGenerator(
             name = "orderSequence",
             sequenceName = "orderId_seq",
             allocationSize = 1,
             initialValue = 1)
     @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 
      "orderSequence")
     public Long id;
 
     @Column(length = 40)
     public String item;
 
     @Column
     public Long price;
 
     @ManyToOne
     @JoinColumn(name = "customer_id")
     @JsonbTransient
     public Customer customer;
 
}

到目前为止,我们已经看到了 Hibernate Panache 提供的一些好处。另一个值得一提的方面是,通过扩展 PanacheEntityBase(或 PanacheEntity),您将能够直接在您的实体上使用一组静态方法。以下表格包含您可以在实体上触发的最常用方法:

Method Description
count Counts this entity from the database (with an optional query and parameters)
delete Delete this entity from the database if it has already been persisted.
flush Flushes all pending changes to the database
findById Finds an entity of this type by ID
find Finds entities using a query with optional parameters and a sort strategy
findAll Finds all the entities of this type
list Shortcut for find().list()
listAll Shortcut for findAll().list()
deleteAll Deletes all the entities of this type
delete Deletes entities using a query with optional parameters
persist Persists all given entities

下面显示了 CustomerRepository 类,它利用了 Customer 实体中可用的新字段和方法:

public class CustomerRepository {
 
     public List<Customer> findAll() {
         return Customer.listAll(Sort.by("id"));
     }
 
     public Customer findCustomerById(Long id) {
         Customer customer = Customer.findById(id);
 
         if (customer == null) {
             throw new WebApplicationException("Customer with id 
              of " +  id + " does not exist.", 404);
         }
         return customer;
     }
     @Transactional
     public void updateCustomer(Customer customer) {
         Customer customerToUpdate = findCustomerById(customer.id);
         customerToUpdate.name = customer.name;
         customerToUpdate.surname = customer.surname;
     }
     @Transactional
     public void createCustomer(Customer customer) {
         customer.persist();
     }
     @Transactional
     public void deleteCustomer(Long customerId) {
         Customer customer = findCustomerById(customerId);
         customer.delete();
     }
 }

最明显的优势是您不再需要 EntityManager 作为代理来管理您的实体类。相反,您可以直接调用实体中可用的静态方法,从而显着降低 Repository 类的冗长性。

为了完整起见,让我们看一下 OrderRepository 类,它也被改编为使用 Panache 对象:

public class OrderRepository {
 
     public List<Orders> findAll(Long customerId) {
         return Orders.list("id", customerId);
     }
 
     public Orders findOrderById(Long id) {
         Orders order = Orders.findById(id);
         if (order == null) {
             throw new WebApplicationException("Order with id of
             " + id  + " does not exist.", 404);
         }
         return order;
     }
     @Transactional
     public void updateOrder(Orders order) {
         Orders orderToUpdate = findOrderById(order.id);
         orderToUpdate.item = order.item;
         orderToUpdate.price = order.price;
     }
     @Transactional
     public void createOrder(Orders order, Customer c) {
         order.customer = c;
         order.persist();
     }
     @Transactional
     public void deleteOrder(Long orderId) {
         Orders order = findOrderById(orderId);
         order.delete();
     }
 }

自从切换到 Hibernate Panache 对我们的 REST 端点和 Web 界面完全透明以来,您的应用程序中没有任何其他变化。使用以下命令像往常一样构建并运行应用程序:

mvn compile quarkus:dev

在控制台上,您应该看到应用程序已启动,并且已添加两个初始客户:

Hibernate: 
    INSERT INTO customer (id, name, surname) VALUES ( nextval('customerId_seq'), 'John','Doe')
Hibernate: 
    INSERT INTO customer (id, name, surname) VALUES ( nextval('customerId_seq'), 'Fred','Smith')
2019-11-28 10:44:02,887 INFO  [io.quarkus] (main) Quarkus 1.0.0.Final started in 2.278s. Listening on: http://[::]:8080
2019-11-28 10:44:02,888 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-postgresql, narayana-jta, resteasy, resteasy-jsonb]

现在,享受由 Hibernate ORM 和 Panache 提供支持的简化 CRUD 应用程序!

Summary

在本章中,我们研究了数据持久性并介绍了著名的 Hibernate ORM 框架。如果您拥有多年的企业编程经验,那么您应该不会发现将相同的概念应用于 Quarkus 具有挑战性。现在,您的整体技能包括使用 Hibernate 及其简化的范式 Panache 配置基于 RDBMS 的应用程序。我们还学习了如何在 OpenShift 集群上的云上部署和连接 RDBMS 和我们的应用程序。

总而言之,我们已经掌握了企业编程的主要支柱(从 REST 服务转向 servlet、CDI 和数据持久性)。

在下一章中,我们将学习如何使用 Quarkus 中的 MicroProfile API 来补充标准企业 API。