实现数据可视化的有效途径(详细指南)

201 阅读8分钟

在这篇博文中,我将向你展示如何开发一个Quarkus应用程序来轻松访问你的关系型数据库(使用Hibernate ORM与Panache),并使用Qute模板在HTML中显示其内容。为了打破它的细节,在这篇博文中你将学习到:

Download the codeDownload the code

  • 什么是Hibernate ORM与Panache
  • 如何用Hibernate ORM with Panache访问关系型数据库。
  • 什么是Qute模板?
  • 如何使用Qute模板在HTML中显示数据库中的数据。
  • 用Tweeter Bootstrap美化Qute模板。

使用案例

假设你有一个存储书籍列表的关系型数据库,你的用户希望有一个可视化的界面来查询书籍列表以及查看书籍的细节。你可以选择一些工具......但你有没有想过用Quarkus来开发?Quarkus允许你轻松地访问关系型数据库,这要归功于带有Panache的Hibernate ORM,以及使用Qute的模板化功能。

下图显示了将在这篇博文中详细介绍的整体架构:

  • Book 实体是一个映射到PostgreSQL数据库的Panache实体(有一个Book 表)。
  • BookPage 是一个Qute资源(一个具有一些Qute特定API的JAX-RS端点),使用两个模板(book.htmlbooks.html )来显示书籍列表和一本书的细节。
  • base.html 是一个基本的Qute模板,包括在其他两个模板中,我们将在其中添加Tweeter Bootstrap

用Hibernate ORM与Panache访问数据库

在Java中,我们有几个API和框架可以将对象映射到关系型数据库。其中一个著名的框架是Hibernate,它实现了JPA规范。Hibernate使Java对象的映射和查询变得简单。Hibernate ORM与Panache是建立在Hibernate之上的,它使简单的映射和简单的查询变得非常简单。

Panache实体

为了将书籍存储到关系型数据库中,我们使用了一个Book Panache实体。像一个标准的JPA实体一样,Book 被注解为@Entity ,并且可以使用其他JPA注解(例如:@Table,@Column, 等等)。但有一点不同的是,Panache实体扩展了PanacheEntity (因此得到一个标识符)或PanacheBaseEntity (因此管理自己的标识符)。

下面的代码显示了Book Panache实体。正如你所看到的,标识符没有@Id 注释,因为它从超类PanacheEntity 继承了它。它被注解为@Entity ,并使用@Table 来指定映射书籍的表的名称,以及@Column 来映射到特定的列。与直接的JPA实体不同的是,所有的属性都可以是公共的,而且getters和setters是可选的:

@Entity
@Table(name = "t_books")
public class Book extends PanacheEntity {

  @Column(length = 100, nullable = false)
  public String title;

  @Column(length = 20)
  public String isbn;

  @Column(nullable = false)
  public BigDecimal price;

  // ....

活动记录模式

通过继承自PanacheBaseEntity ,我们的Book 实体从许多允许CRUD操作和查询的方法中获益。这就是所谓的主动记录模式。这允许你做以下事情:

Book.findById(id);
Book.findByIdOptional(id);
Book.deleteById(id);
Book.deleteAll();
Book.count();

这是针对CRUD操作的,但是PanacheBaseEntity 也有一组方法来查询实体,对它们进行排序和分页:

Book.list(query);
Book.find(query).list();
Book.find(query, Sort.by(sort)).list();
Book.find(query, Sort.by(sort)).page(pageIndex, pageSize).list();

Hibernate ORM with Panache还简化了JPA中定义的Java Persistence Query Language(JPQL)。它允许你在编写查询时不使用SELECTFROM 子句。你只专注于WHERE 子句(无需使用WHERE 关键字):

Book.list("price < 10", Sort.by("isbn"));
Book.list("price < 10 and nbOfPages > 100");
Book.list("price < 10 and nbOfPages > 100", Sort.by("isbn"));
Book.find("price < 10 and nbOfPages > 100", Sort.by("isbn")).list();
Book.find("price < 10 and nbOfPages > 100", Sort.by("isbn")).page(2, 4).list();

用Qute实现数据的可视化

现在我们有了一个带有查询方法的Book 实体,让我们添加一些Qute资源来可视化书籍的列表和书籍的细节。Qute另一个非常适合Quarkus的模板化引擎。Quarkus的文档中明确指出:

Qute是一个专门为满足Quarkus需要而设计的模板引擎。反射的使用被最小化,以减少本地图像的大小。该API结合了命令式和非阻塞反应式的编码风格。

声明Qute模板

有不同的方法来声明Qute模板,但我相当喜欢类型安全的方法。这个想法是创建一个Qute资源(这里称为BookPage ),并用代码定义模板。在下面的例子中,我们定义了两个模板,叫做book()books()

注意,这些方法可以接受参数。book() 方法需要一个book 对象,因此它可以访问book 的属性,并在HTML中显示它们。至于books() ,它需要一个书的列表,模板将迭代并显示:

@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {
   @CheckedTemplate
   public static class Templates {
     public static native TemplateInstance book(Book book);
     public static native TemplateInstance books(List<Book> books);
   }
   // ...

类型安全的方法依赖于一些约定。Qute模板必须与代码中定义的名称相同(例如,book()book.html )。然后,它们必须位于/src/main/resources/templates 目录下,在一个以Qute资源命名的子目录下(这里是BookPage ):

显示书的细节

为了显示一本书的细节,我们现在需要创建一个方法来访问数据库,给出一个书的标识符。在BookPage 资源中,注意showBookById() 方法。它使用了JAX-RS注释(@GET,@Path,@PathParam("id")),所以它可以处理HTTP请求,如http://localhost:8080/page/books/2。注意到Book.findById(id) 是如何使用Active Record模式来通过其标识符获得Book 实体的:

@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {

  @CheckedTemplate
  public static class Templates {
    public static native TemplateInstance book(Book book);
  }

  @GET
  @Path("/{id}")
  public TemplateInstance showBookById(@PathParam("id") Long id) {
    return Templates.book(Book.findById(id));
  }
  // ...

一旦找到这本书,它就会被返回到book() 的模板中。然后,Qute引擎将在src/main/resources/templates/BookPage/book.html 下寻找模板,并将book 对象作为一个参数传递。下面的book.html 文件非常简单。它使用表达式语言来访问book 对象的属性:{book.id} 访问book 对象的id 属性,该对象在Templates.book(Book.findById(id)) 中被传递:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Book</title>
</head>
<body>
  Id: {book.id}
  Title: {book.title}
  Description: {book.description}
  Price: {book.price}
  Isbn: {book.isbn}
  Number of Pages: {book.nbOfPages}
  Publication Date: {book.publicationDate}
  Created Date: {book.createdDate}
</body>
</html>

显示书籍列表

为了显示书籍的列表,我们需要查询它们。这时,Panache使生活变得简单。showAllBooks() 方法接受所需的参数来执行一个带有排序和分页的查询。因此,例如,如果你用下面的HTTP请求来调用showAllBooks()

http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=1&size=5

它将执行以下Panache查询:

Book.find("price < 50 and nbOfPages > 100", Sort.by("isbn")).page(1, 5))
@Path("/page/books")
@Produces(MediaType.TEXT_HTML)
@ApplicationScoped
public class BookPage {

  @CheckedTemplate
  public static class Templates {
    public static native TemplateInstance books(List<Book> books);
  }

  @GET
  public TemplateInstance showAllBooks(
          @QueryParam("query") String query,
          @QueryParam("sort") @DefaultValue("id") String sort,
          @QueryParam("page") @DefaultValue("0") Integer pageIndex,
          @QueryParam("size") @DefaultValue("1000") Integer pageSize) {
    return Templates.books(Book.find(query, Sort.by(sort)).page(pageIndex, pageSize).list())
      .data("query", query)
      .data("sort", sort)
      .data("pageIndex", pageIndex)
      .data("pageSize", pageSize);
  }
  // ...

注意,当我们调用books() 模板时,我们传递查询返回的书籍列表,但我们也将参数添加到模板中(例如:data("sort", sort) )。这是另一种向模板传递数据的方式。

模板循环浏览图书列表({#for book in books} ),并显示属性({book.isbn},{book.id} )。为了显示传递给模板的数据(data("sort", sort)),我们使用特殊的data 命名空间({data:sort}):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Books</title>
</head>
<body>
<code>Book.find({data:query}, Sort.by({data:sort})).page({data:pageIndex}, {data:pageSize}).list()</code>
<table>
  <thead>
  <tr>
    <th scope="col">#</th>
    <th scope="col">Title</th>
    <th scope="col">Isbn</th>
    <th scope="col">Price</th>
    <th scope="col">n° Pages</th>
    <th scope="col">Publication Date</th>
  </tr>
  </thead>
  <tbody>
  {#for book in books}
    <tr>
      <th scope="row"><a href="http://localhost:8080/page/books/{book.id}">{book.id}</a></th>
      <td>{book.title}</td>
      <td>{book.isbn}</td>
      <td>{book.price}</td>
      <td>{book.nbOfPages}</td>
      <td>{book.publicationDate}</td>
    </tr>
  {/for}
  </tbody>
</table>
</body>
</html>

美化模板

为了美化这两个模板,我们可以使用一个两者都会包含的基础模板。这是添加一些Tweeter Bootstrap(如果需要的话,CSS甚至是JavaScript)的完美位置。这就是base.html 的作用。它还使用了一个#insert 部分,用来指定可以被子模板覆盖的部分:这里是包含模板的标题({#insert title})和主体({#insert body}):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
  <title>{#insert title}Default Title{/}</title>
</head>
<body>
<div class="container">
  <h1>{#insert title}Default Title{/}</h1>
  {#insert body}No body!{/}
</div>
</body>
</html>

要将base.html 模板包括在book.htmlbooks.html 模板中,只需包括它 ({#include base.html}) 并覆盖特定的部分 ({#title}{/title}{#body}{/body}):

{#include base.html}
{#title}{books.size} Books{/title}
{#body}
  <!-- body -->
{/body}
{/include}

执行应用程序

要执行代码,你需要Docker启动并运行。为什么?因为数据被存储在PostgreSQL数据库中,我们使用Quarkus DevServices来自动启动它。DevServices的工作方式是,它检测到应用程序需要PostgreSQL数据库(因为它被声明为pom.xml ),并在幕后使用TestContainers,下载Docker镜像,启动和停止它:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>

这只需通过以下命令来启动Quarkus:

$ mvn quarkus:dev

然后,你可以将你的浏览器指向以下URL,这样你就可以查询数据库并显示不同的书籍列表:

http://localhost:8080/page/books?query=price < 10
http://localhost:8080/page/books?query=price < 10 and nbOfPages > 100 &sort=isbn
http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=1&size=5
http://localhost:8080/page/books?query=price < 50 and nbOfPages > 100 &sort=isbn&page=2&size=5

要获得一本书的详细信息,你可以使用以下网址:

http://localhost:8080/page/book/1
http://localhost:8080/page/book/2

结论

有几种方法可以轻松显示数据库中的数据。在这篇博文中,我想向你展示QuteHibernate ORM与Panache的结合。Qute是一个模板引擎,所以它的主要目的不是成为又一个前端框架。你可以用Qute模板来写电子邮件,发送消息等。但是写HTML页面也是可以的:

Download the codeDownload the code

至于Hibernate ORM与Panache,它是使简单查询变得微不足道的好方法。记住,Hibernate ORM with Panache是建立在JPA之上的。所以如果你需要JPA的力量,你可以。通过继承PanacheEntity ,你可以访问JPA的EntityManager ,并可以随时使用它。通过Panache,你并不拘泥于Hibernate ORM,你还拥有JPA的全部权限。

现在轮到你了下载代码并试一试吧。

参考资料

如果你想尝试一下这段代码,请从GitHub上下载,构建它,运行它,并确保打破微服务之间的通信,以看到回退的作用: