如何用Java、Spring、JPA和CockroachDB构建一个云原生网络应用程序

8,530 阅读14分钟

为了学习如何将CockroachDB与Hibernate这样的JPA提供者一起使用,让我们建立一个Spring Boot应用,在CockroachDB无服务器平台上托管的CockroachDB数据库中存储视频游戏排行榜的详细信息。

这个Spring Boot应用将包含两个HTML页面,并将使用Heroku部署到云端。

在开始创建应用程序之前,先花点时间确认一下我们是否有。

我们的排行榜应用程序使用CockroachDB无服务器平台从Cockroach数据库中存储和检索其数据。

为了准备应用程序的资源,我们将检索CockroachDB集群的连接细节。使用计算机终端,我们将连接到集群并为排行榜创建一个数据库。

检索CockroachDB无服务器上的数据库连接信息

如果你已经是一个拥有集群的CockroachDB用户,打开集群并点击集群概览页面上的连接按钮。这将打开 "连接"窗口。

如果你是CockroachDB的新用户,请创建一个账户并按照入职提示在测试模式下创建一个免费的无服务器集群。

然后你会被要求创建一个SQL用户。复制并保存你的密码在一个安全的地方。

点击 "下一步",查看 "连接"窗口。在这里,你可以找到连接信息,包括你的用户名、主机、数据库和端口。

如何建立一个集群连接

在 "连接"窗口中,选择 "选择选项/语言"下的 "一般连接字符串"。一个操作系统将被自动检测,但我们可以通过下拉菜单选择另一个操作系统。

我们被指示运行的命令将取决于我们使用的是哪个操作系统。

模式中的下一个命令将是使用已安装的CockroachDB客户端从我们的命令行连接到已创建的集群上。在选择选项/语言下选择CockroachDB客户端

执行该命令后,集群的详细信息将显示在你的控制台。

创建一个数据库

在这一点上,我们有一个活跃的Cockroach shell,在计算机和CockroachDB集群之间建立了一个连接。现在我们将通过运行中的shell来执行SQL语句,创建一个数据库,并为排行榜应用程序建立模型来存储数据。

下面的步骤概述了在我们的CockroachDB集群中创建数据库所需的SQL语句。

首先,执行下面的命令,创建一个名为leaderboard的数据库。

CREATE DATABASE leaderboard;

接下来,执行USE 命令,将默认的集群数据库设置为你创建的 leaderboard 数据库。该命令将导致后续的SQL语句在leaderboard数据库上执行。

USE leaderboard;

执行下面的SQL语句,在数据库中创建一个名为leaderboard_entries 的表。

CREATE TABLE leaderboard_entries (
  id INT DEFAULT unique_rowid(),
  game_name VARCHAR(200) NOT NULL,
  game_score INT DEFAULT 0,
  player_username VARCHAR(200) NOT NULL,
  team_size INT DEFAULT 1,
  created_at TIMESTAMP DEFAULT now(),
  updated_at TIMESTAMP DEFAULT now()
);

创建的表将存储每个排行榜记录的详细信息。当一条记录被插入时,unique_rowid() 函数将生成一个唯一的数字作为id 字段的值。

现在,我们将创建一个Spring Boot项目来使用Cockroach Serverless上托管的数据库。这个Spring Boot项目将从leaderboard 数据库中存储和检索数据。

引导一个Spring Boot项目

Spring Boot是一个框架,它减少了启动一个生产就绪的Spring项目所需的配置量。

下面的步骤将指导你使用Spring Boot命令行界面(CLI)构建一个Spring Boot项目。另外,你也可以使用Spring-initialzr

要开始,在一个新的目录中生成一个名为leaderboard的Spring Boot网络模板,并进入该目录。

spring init --dependencies=web leaderboard

cd leaderboard

然后,使用已安装的Maven CLI构建并运行该模板应用程序。

mvn spring-boot:run

启动后,Spring Boot应用程序将可通过我们的本地浏览器查看,网址是http://localhost:8080 。当通过我们的网络浏览器打开时,将显示404错误信息,因为应用程序中没有创建路由。我们将在本教程的后面部分创建两条路由。

安装项目的依赖性

现在我们已经生成了一个Spring Boot应用程序,我们将安装构建排行榜应用程序所需的所有依赖项。

首先,在Java的集成开发环境(IDE)中打开leaderboard 项目。

leaderboard 项目中打开pom.xml 文件,将下面的代码添加到依赖项元素中。

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-core</artifactId>
</dependency>

<dependency>
   <groupId>org.postgresql</groupId>
   <artifactId>postgresql</artifactId>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-loader</artifactId>
</dependency>

上面的代码在Spring Boot项目中添加了以下依赖项。

  • 用于连接Cockroach数据库的Postgres JDBC驱动程序
  • 用于将Hibernate作为JPA提供者的Hibernate-Core
  • Thymeleaf Java模板引擎,用于在Spring Boot应用程序的HTML页面中渲染视图和数据。

接下来,停止运行中的Spring Boot应用程序,执行下面的Maven命令,安装添加到pom.xml 文件中的新依赖项。

mvn clean install -DskipTests

注意-DskipTests 标志将使Maven跳过为该项目生成的模板测试套件的运行。

添加数据库凭证

现在是时候添加我们在CockroachDB无服务器上创建的数据库集群的连接细节了。

Spring Boot的一个好处是,它在使用Hibernate作为Java Persistence API(JPA)提供者时需要的配置很少。你只需要在你的pom.xml 文件中包含它,并指定数据库连接的细节。

我们将使用下面的步骤来指定Cockroach数据库的JDBC URL,并指定Hibernate要使用的CockroachDB方言。

Cockroach无服务器平台中的连接模式提供了格式化JDBC URL的功能,以作为application.properties 文件中的HOST_URL

导航到CockroachDB无服务器平台并重新加载,查看最近创建的排行榜数据库,然后点击连接按钮,启动连接模式。

为了格式化JDBC URL,点击数据库下拉菜单,选择排行榜数据库。

接下来,点击选择选项/语言下拉菜单,选择Java选项。

正如下面图片的下部所强调的,只复制JDBC URL中没有sslrootcert 选项的部分,因为我们不会使用证书授权(CA)文件。

打开leaderboard/src/main/resources 目录中的application.properties 文件。这个文件将指定数据库的连接细节和其他一些配置,包括Hibernate的CockroachDB方言。

将下面的代码块添加到application.properties 文件中。确保用上面获得的JDBC URL替换下面的HOST_URL, 占位符,同时用我们的集群密码替换 "输入密码秘密"占位符。

spring.datasource.url=jdbc:HOST_URL

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.CockroachDB201Dialect

spring.jpa.show-sql=true
spring.datasource.dbcp2.test-while-idle=true
spring.datasource.dbcp2.validation-query=select 1

spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

一个Spring Boot应用程序包括一个控制器、存储库类、服务层和域。我们将在src/main/java/com/example/leaderboard 目录中创建四个包来包含这些组件。

创建第一个名为entities 的目录,并在其中创建一个Leaderboard.java 文件。然后,将下面的代码添加到entities/Leaderboard.java 文件中,以建立应用程序的域层和Cockroach数据库的模型。

package com.example.leaderboard.entities;

import javax.persistence.*;

@Entity
@Table(name="leaderboard_entries")
public class Leaderboard {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "game_name")
    private String gameName;

    @Column(name = "game_score")
    private String gameScore;

    @Column(name = "player_username")
    private String playerUsername;

    @Column(name = "team_size")
    private String teamSize;

    @Column(name = "created_at")
    private String createdAt;

    // setters
    public void setId(Long id) {
        this.id = id;
    }

    public void setGameName(String gameName) {
        this.gameName = gameName;
    }

    public void setPlayerUsername(String playerUsername) {
        this.playerUsername = playerUsername;
    }

    public void setTeamSize(String teamSize) {
        this.teamSize = teamSize;
    }

    public void setCreatedAt(String createdAt) {
        this.createdAt = createdAt;
    }

    public void setGameScore(String gameScore) {
        this.gameScore = gameScore;
    }

    // getters
    public Long getId() {
        return id;
    }

    public String getGameScore() {
        return gameScore;
    }

    public String getGameName() {
        return gameName;
    }

    public String getPlayerUsername() {
        return playerUsername;
    }

    public String getTeamSize() {
        return teamSize;
    }

    public String getCreatedAt() {
        return createdAt;
    }
}

上面的类使用了几个访问器来建立Cockroach数据库中的leaderboard_entries 表的模型。

接下来,在src/main/java/com/example/leaderboard 目录下创建第二个包,名为repositories 。然后,在repositories 包中创建一个LeaderboardRepository.java 文件。

将下面的代码添加到LeaderboardRepository.java 文件中,创建一个扩展了JpaRepository 接口的接口。你将使用从JpaRepository 接口继承的方法,在与Cockroach数据库交互时执行CRUD操作。

package com.example.leaderboard.repositories;

import com.example.leaderboard.entities.Leaderboard;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface LeaderboardRepository extends JpaRepository { }

src/main/java/com/example/leaderboard directory 中创建第三个名为services 的包。

在新的services 包内,创建一个LeaderboardService.java 文件来存储应用程序的业务逻辑。然后这将通过Spring的@Autowired 注释使用LeaderboardRepository 接口。

现在,将下面的代码添加到LeaderboardService.java 文件中,以建立应用程序的业务逻辑,将排行榜的详细信息添加到数据库中。

package com.example.leaderboard.services;
import com.example.leaderboard.entities.Leaderboard;
import com.example.leaderboard.repositories.LeaderboardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.stereotype.Service;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import java.util.List;

@Service
@Configurable
public class LeaderboardService {
    @Autowired
    LeaderboardRepository leaderboardRepository;

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void insertLeaderboard(Leaderboard leaderboardData) {
        entityManager.createNativeQuery(
               "INSERT INTO leaderboard_entries (game_name, game_score, player_username, team_size) VALUES (?,?,?,?)")
               .setParameter(1, leaderboardData.getGameName())
               .setParameter(2, leaderboardData.getGameScore())
               .setParameter(3, leaderboardData.getPlayerUsername())
               .setParameter(4, leaderboardData.getTeamSize())
               .executeUpdate();
    }

    public LeaderboardService() {}

    public List getLeaderboard() {
        return leaderboardRepository.findAll();
    }
}

通过阅读上面的代码,我们会发现getLeaderboardEntries 方法负责检索leaderboard_entries 表中的所有行,而insertLeaderboard 方法则是将新的排行榜记录插入数据库。

最后,在/src/main/java/com/example/leaderboard 目录下创建最后一个名为controller 的包。然后,在controller 包内创建一个LeaderboardController.java 文件。

LeaderboardController.java 文件中的代码将被用来管理用户通过网络浏览器发出的API请求。我们将使用Thymeleaf在用户向默认(/)或/create-entry endpoints 发出请求时用HTML内容进行响应。

将下面的代码块添加到LeaderboardController.java 文件中。

package com.example.leaderboard.controller;
import com.example.leaderboard.entities.Leaderboard;
import com.example.leaderboard.services.LeaderboardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import org.springframework.ui.Model;

@Controller
public class LeaderboardController {
    @Autowired
    LeaderboardService leaderboardService;

    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("entries", leaderboardService.getLeaderboard());

        return "home";
    }

    @GetMapping("/status")
    public String status() {
        return "status";
    }

    @GetMapping("/create-entry")
    public String getEntries(@ModelAttribute Leaderboard leaderboard, Model model) {
        model.addAttribute("leaderboard", new Leaderboard());

        return "create-entry";
    }

    @PostMapping("/create-entry")
    public String submitEntryData(Leaderboard leaderboardData, Model model) {
        model.addAttribute("leaderboard", leaderboardData);
        leaderboardService.insertLeaderboard(leaderboardData);

        return "redirect:/";
    }
}

这时,src/main/java/com/example/leaderboard 目录应该包含你刚刚创建的四个目录和五个文件。

Thymeleaf是一个现代的Java模板引擎,对Spring Web应用有很大的支持。你将使用Thymeleaf的标准表达式语法来格式化和显示你应用程序中每个排行榜的细节。

在前面的步骤中,我们通过安装Thymeleaf依赖项将Thymeleaf添加到应用程序中。LeaderboardController ,还包含了URL映射和要显示的视图。

接下来,我们将在应用程序的src/main/resources 目录中创建两个HTML文件和一个CSS文件。

src/main/resources/templates 目录中创建一个home.html 文件,作为Spring Leaderboard应用程序的默认页面呈现。

接下来,将下面的HTML代码添加到home.html 文件中。

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
   <title>Getting Started: Serving Web Content</title>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link th:href="@{/css/main.css}" rel="stylesheet">
</head>

<body>
<nav class="flex">
   <p> All Leaderboard Entries </p>

   <button>
       <a href="/create-entry">
           Create New Entry
       </a>
   </button>
</nav>

<div class="container">
   <div class="content">
       <table style="width: 100%; text-align: center;">
           <tr style="border-bottom: 1px solid #000">
               <th>Game Name</th>
               <th>Game Score</th>
               <th>Player Name</th>
               <th>Team Size</th>
               <th>Created</th>
           </tr>

           <tr th:each="leaderboard: ${entries}">
               <td th:text="${leaderboard.gameName}"></td>
               <td th:text="${leaderboard.gameScore}"></td>
               <td th:text="${leaderboard.playerUsername}"></td>
               <td th:text="${leaderboard.teamSize}"></td>
               <td th:text="${leaderboard.createdAt}"></td>
           </tr>
       </table>
   </div>
</div>

</body>
</html>

上面的 HTML 代码将呈现索引控制器中使用的 getLeaderboardEntries 方法中的所有排行榜。使用 Thymeleaf 中的 th:each 表达式在排行榜上进一步执行迭代,以在表格中显示每个排行榜。

接下来,我们将制作一个表单,用户将填写该表单以添加排行榜条目。使用以下代码在模板目录中创建一个create-entry.html文件。

<!DOCTYPE HTML>
<html>
<head>
   <title>CockroachDB Java Leaderboard</title>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link th:href="@{/css/main.css}" rel="stylesheet">
</head>

<body>
<nav class="flex">
   <p> All Entries </p>

   <button>
       <a href="/">
           View All Leaderboards
       </a>
   </button>
</nav>

<div class="container">
   <form th:action="@{/create-entry}" th:object="${leaderboard}" method="post">
       <h2 class="align-center"> Create New Leaderboard </h2>
       <hr/>

       <br/>
       <div>
           <label for="leaderboardName">
               Leaderboard Name
           </label>
           <br/>
           <input
                   type="text"
                   th:field="*{gameName}"
                   id="leaderboardName"
                   placeholder="Leaderboard Name"
>
           <br/>
       </div>

       <div>
           <label for="username">
               Player Username
           </label>
           <br/>
           <input
                   type="text"
                   th:field="*{playerUsername}"
                   id="username"
                   placeholder="Your Player Username"
>
       </div>

       <div>
           <label for="teamSize">
               Team Size
           </label>
           <br/>

           <input
                   type="number"
                   th:field="*{teamSize}"
                   id="teamSize"
                   placeholder="Your Team Size"
>
       </div>

       <div>
           <label for="gameScore">
               Game Score
           </label>
           <br/>

           <input
                   type="number"
                   th:field="*{gameScore}"
                   id="gameScore"
                   placeholder="Your Game Score"
>
       </div>

       <br/>
       <button type="submit">Create Leaderboard</button>
   </form>
</div>
</body>

</html>

th:action="@{/create-entry}" 表达式指导表单元素将用户的输入提交到/create-entry 端点。gameName,playerUsername,teamSize, 和gameScore 字段与你在getEntries 控制器中创建的Leaderboard 对象的字段相匹配。

接下来,在/src/main/resources/static 目录中创建一个名为css 的目录。继续在static/css 目录下创建一个名为main.css 的样式表文件。这个CSS文件的目的是为默认页面和/create-entry

将下面的CSS代码添加到main.css 文件中。

.flex {
   display: flex;
   justify-content: space-between;
}

.align-center {
   text-align: center;
}

nav {
   border-bottom: 1px solid grey;
   padding: 0 1rem;
}

form {
   display: flex;
   flex-direction: column;
}

input {
   margin: .5rem 0;
   height: 35px;
   width: 95%;
   border: 1px solid #000;
   border-radius: 5px;
   padding: .5rem .5rem;
   color: #000;
}

.container {
   display: flex;
   justify-content: center;
   align-items: center;
   background-color: #F5F7FA;
   height: calc(100vh - 50px);
}

.content {
   width: 940px;
   height: calc(100vh - 100px);
   background: white;
   padding: 1rem;
   border-radius: 10px;
}

tr, th, td {
   padding: 20px;
}

tr:nth-child(even) {
   background-color: #D6EEEE;
}

tr {
   border-bottom: 5px solid #ddd;
}

form {
   background-color: #fff;
   border-radius: 5px;
   box-shadow: 0 2px 3px grey;
   width: 30rem;
   padding: 1rem;
}

button {
   height: 40px;
   border: 1px solid grey;
   border-radius: 5px;
   color: #000;
}

button:hover {
   cursor: pointer;
}

a {
   text-decoration: none;
}

Heroku是一个基于云的平台即服务(PaaS),用于部署用各种编程语言编写的Web应用程序和API服务。

对于Java应用程序,Heroku提供了构建包,它使用Maven下载依赖项并构建应用程序,并使用OpenJDK运行应用程序。

使用heroku-buildpack-java ,将从使用Git CLI创建的本地git仓库编译和部署排行榜项目。

在开始与Heroku的部署过程之前,打开leaderboard项目中的pom.xml 文件,将项目的Java版本改为与Heroku兼容的值。

用下面的代码替换java.version 这个元素。

1.8

创建本地和远程存储库

Git是一个基于CLI的工具,用于跟踪更改,而GitHub是一个基于云的服务,用于将项目相关的文件存储在一个仓库中。在这个演示中,我们将使用Git和GitHub的CLI工具创建一个本地和远程的git仓库。

执行下面的命令,在leaderboard 项目中初始化一个本地git仓库。

git init

接下来,执行下面的命令,创建一个包含目标文件夹的.gitignore 文件。

echo "target" > .gitignore

现在,执行add命令,将leaderboard 项目中的所有文件添加到创建的本地仓库。

git add .

使用下面的命令,我们将提交你在leaderboard 本地仓库所做的最后一次修改。

git commit -m “feat: built an MVP of the leaderboard project”

使用GitHub官方(gh)CLI工具,我们将创建一个远程仓库,将leaderboard 项目文件存储在云端。

第一次使用时,该命令会提示你使用我们的GitHub个人访问令牌或通过浏览器窗口进行认证。

执行下面的gh 命令,创建一个公共仓库来存储leaderboard 的代码库。用你的GitHub账户用户名替换GITHUB_USERNAME ,以确保我们创建的仓库有一个唯一的名字。

gh repo create-spring-leaderboard --public

执行下面的命令,指定创建的仓库的URL作为Spring-leaderboard的远程源。

git remote add origin https://github.com//-spring-leaderboard.git

开始,我们将使用计算机终端在Heroku上创建一个应用程序。Heroku应用程序将作为一个容器,用于存储我们将在后面的步骤中部署的领导板应用程序。

执行下面的命令来创建一个Heroku应用程序。如果我们没有经过认证,Heroku会提示我们按一个键,通过网络浏览器进行认证。

heroku create

接下来,执行下面的命令,将leaderboard 项目的代码库推送到Heroku。文件推送后,Heroku将立即使用Maven启动构建过程。

git push heroku main

应用程序部署到Heroku后,部署URL将在你电脑上的Heroku部署日志中打印出来。你可以在已部署的排行榜应用程序的下图中看到突出显示的部署URL。

根据排行榜项目的配置,Heroku将自动创建并使用Postgres数据库插件,以使部署过程更容易。在这种情况下,不需要Heroku的数据库插件,因为我们使用的是CockroachDB。

执行下面的addon命令,销毁数据库addon。

heroku addons:destroy heroku-postgresql

使用Web浏览器,导航到排行榜应用程序的部署URL,创建我们的第一个排行榜。

在默认页面上,我们会发现排行榜的标题没有任何列表条目。

点击创建新条目按钮,导航到create-entry 页面。我们将在这个页面上为我们的第一个排行榜输入细节。

create-entry 页面,将排行榜名称、球员用户名、球队得分和比赛得分细节添加到表格的输入字段中。

接下来,点击 "创建排行榜"按钮,提交输入的详细信息。

点击按钮后,细节将被保存到数据库,应用程序将重定向到列出所有创建的排行榜条目的默认页面。

你也可以通过一个活跃的Cockroach shell查询数据库来查看排行榜的细节。

就这样,我们的排行榜应用程序功能齐全,并在CockroachDB中存储数据。

总结

恭喜你完成了本教程!我们已经建立了一个排行榜应用程序。我们已经建立了一个排行榜应用程序,将数据存储在CockroachDB无服务器集群中的CockroachDB数据库中。

我们首先从CockroachDB无服务器中获取了CockroachDB集群的连接细节,然后从本地终端使用CockroachDB客户端连接到集群并创建数据库。接下来,我们使用Spring CLI来引导一个模板式的Spring Boot Web应用,同时使用Maven来安装所需的依赖。为了测试完成的排行榜应用程序,我们使用Heroku CLI部署了它。这使我们能够插入和查看排行榜样本的细节。

CockroachDB的无服务器化大大简化了为我们的应用创建集群的过程。在本地构建排行榜应用的同时,我们可以通过使用CockroachDB无服务器来实现设置你的CockroachDB集群的最佳体验。