Build a Dashboard Using Cassandra, Astra, and Stargate – 使用Cassandra、Astra和Stargate建立一个仪表板

最后修改: 2021年 5月 27日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

In this article, we are going to build “Tony Stark’s Avengers Status Dashboard”, used by The Avengers to monitor the status of the members of the team.

在这篇文章中,我们将建立 “托尼-斯塔克的复仇者联盟状态仪表盘”,复仇者联盟用它来监控团队成员的状态。

This will be built using DataStax Astra, a DBaaS powered by Apache Cassandra using Stargate to offer additional APIs for working with it. On top of this, we will be using a Spring Boot application to render the dashboard and show what’s going on.

这将使用DataStax Astra构建,这是一个由Apache Cassandra提供的DBaaS,使用Stargate以提供额外的API用于工作。在此之上,我们将使用一个Spring Boot应用程序来渲染仪表盘并显示正在发生的事情。

We will be building this with Java 16, so make sure this is installed and ready to use before continuing.

我们将用Java 16构建这个软件,所以在继续之前,请确保这个软件已经安装并准备好使用。

2. What Is Astra?

2.什么是阿斯特拉?

DataStax Astra is a Database as a Service offering that is powered by Apache Cassandra. This gives us a fully hosted, fully managed Cassandra database that we can use to store our data, which includes all of the power that Cassandra offers for scalability, high availability and performance.

DataStax Astra是一个由Apache Cassandra提供的数据库即服务产品。这为我们提供了一个完全托管、完全管理的 Cassandra 数据库,我们可以用它来存储我们的数据,其中包括 Cassandra 为可扩展性、高可用性和性能提供的所有功能。

On top of this, Astra also incorporates the Stargate data platform that exposes the exact same underlying data via different APIs. This gives us access to traditional Cassandra tables using REST and GraphQL APIs – both of which are 100% compatible with each other and the more traditional CQL APIs. These can make access to our data incredibly flexible with only a standard HTTP client – such as the Spring RestTemplate.

在此基础上,Astra还结合了Stargate数据平台,通过不同的API暴露出完全相同的基础数据。这让我们可以使用REST和GraphQL API来访问传统的Cassandra表–这两种API都是100%相互兼容的,而且是更传统的CQL API。这些可以使我们对数据的访问变得无比灵活,只需一个标准的HTTP客户端–比如Spring RestTemplate

It also offers a JSON Document API that allows for much more flexible data access. With this API there is no need for a schema, and every record can be a different shape if needed. Additionally, records can be as complex as needed, supporting the full power of JSON for representing the data.

它还提供一个JSON文档API,允许更灵活的数据访问。有了这个API,就不需要模式了,如果需要,每条记录都可以是不同的形状。此外,记录可以根据需要复杂化,支持JSON的全部功能来表示数据。

This does come with a cost though – the Document API is not interchangeable with the other APIs, so it is important to decide ahead of time how data needs to be modeled and which APIs are best used to access it.

不过这也是有代价的–文档API不能与其他API互换,所以提前决定需要如何对数据进行建模以及最好使用哪些API来访问数据是很重要的。

3. Our Application Data Model

3.我们的应用数据模型

We are building our system around the Astra system on top of Cassandra. This will have a direct reflection on the way that we model our data.

我们正在围绕Cassandra之上的Astra系统构建我们的系统。这将直接反映在我们的数据建模方式上。

Cassandra is designed to allow massive amounts of data with very high throughput, and it stores records in tabular form. Astra adds to this some alternative APIs – REST and GraphQL – and the ability to represent documents as well as simple tabular data – using the Document API.

Cassandra被设计成允许以非常高的吞吐量处理大量的数据,并且它以表格形式存储记录。Astra在此基础上增加了一些替代的API–REST和GraphQL–以及表示文档和简单表格数据的能力–使用文档API。

This is still backed by Cassandra, which does schema design differently. In modern systems, space is no longer a constraint. Duplicating data becomes a non-issue, removing the need for joins across collections or partitions of data. This means that we can denormalize our data within our collections to suit our needs.

这仍然是由Cassandra支持的,它以不同的方式进行模式设计。在现代系统中,空间不再是一个限制因素。重复数据成为一个非问题,不再需要跨集合或数据分区的连接。这意味着我们可以在我们的集合中对数据进行反规范化以满足我们的需求。

As such, our data model is going to be built around two collections – events and statuses. The events collection is a record of every status event that has ever happened – this can potentially get very large, something for which Cassandra is ideal. This will be covered in more detail in the next article.

因此,我们的数据模型将围绕两个集合建立–eventsstatusesevents集合是曾经发生过的每一个状态事件的记录–这可能会变得非常大,这正是 Cassandra 的理想之处。这将在下一篇文章中详细介绍。

Records in this collection will look as follows:

在这个集合中的记录将看起来如下。

avenger falcon
timestamp 2021-04-02T14:23:12Z
latitude 40.714558
longitude -73.975029
status 0.72

This gives us a single event update, giving the exact timestamp and location of the update and a percentage value for the status of the Avenger.

这给了我们一个单一的事件更新,给出更新的确切时间戳和位置,以及复仇者状态的百分比值。

The statuses collection contains a single document that contains the dashboard data, which is a denormalized, summarized view of the data that goes into the events collection. This document will look similar to this:

statuses集合包含一个单一的文档,其中包含仪表盘数据,这是进入events集合的数据的一个去规范化的、汇总的视图。这个文件将看起来类似于这样。

{
    "falcon": {
	"realName": "Sam Wilson",
	"location": "New York",
	"status": "INJURED",
	"name": "Falcon"
    },
    "wanda": {
        "realName": "Wanda Maximoff",
        "location": "New York",
        "status": "HEALTHY"
    }
}

Here we have some general data that doesn’t change – the name and realName fields – and we have some summary data that is generated from the most recent event for this Avenger – location is derived from the latitude and longitude values, and status is a general summary of the status field from the event.

这里我们有一些不会改变的一般数据–namerealName字段–我们还有一些从这个复仇者最近的事件中产生的摘要数据–location来自latitudelongitude值,而status是事件中status字段的一般总结。

This article is focused on the statuses collection, and accessing it using the Document API. Our next article will show how to work with the events collection which is row-based data instead.

这篇文章的重点是statuses集合,以及使用Document API访问它。我们的下一篇文章将展示如何使用events集合,它是基于行的数据。

4. How to Set Up DataStax Astra

4.如何设置DataStax Astra

Before we can start our application, we need a store for our data. We are going to use the Cassandra offering from DataStax Astra. To get started, we need to register a free account with Astra and create a new database. This needs to be given a reasonable name for both the database and the keyspace within:

在开始我们的应用程序之前,我们需要一个数据存储。我们将使用 DataStax Astra 的 Cassandra 产品。为了开始工作,我们需要在 Astra 注册一个免费帐户并创建一个新的数据库。这需要为数据库和其中的键空间赋予一个合理的名称。

(Note – screens are accurate at the time of publication but might have changed since)

(注意–屏幕在发布时是准确的,但之后可能会有变化)

This will take a few minutes to set up. Once this is done, we will need to create an access token.

这将需要几分钟的时间来设置。一旦完成,我们将需要创建一个访问令牌。

In order to do this, we need to visit the “Settings” tab for the newly created database and generate a token:

为了做到这一点,我们需要访问新创建的数据库的 “设置 “选项卡并生成一个令牌。

Once all of this is done, we will also need our database details. This includes:

一旦所有这些都完成,我们还需要我们的数据库细节。这包括。

  • Database ID
  • Region
  • Keyspace

These can be found on the “Connect” tab.

这些可以在 “连接 “标签上找到。

Finally, we need some data. For the purposes of this article, we are using some pre-populated data. This can be found in a shell script here.

最后,我们需要一些数据。为了本文的目的,我们使用一些预先填充的数据。这可以在一个shell脚本中找到这里

5. How to Set Up Spring Boot

5.如何设置Spring Boot

We are going to create our new application using Spring Initializr; we’re also going to use Java 16 – allowing us to use Records. This in turn means we need Spring Boot 2.5 – currently this means 2.5.0-M3.

我们将使用Spring Initializr创建我们的新应用程序;我们还将使用Java 16 – 允许我们使用Records这又意味着我们需要Spring Boot 2.5 – 目前这意味着2.5.0-M3。

In addition, we need Spring Web and Thymeleaf as dependencies:

此外,我们还需要Spring Web和Thymeleaf作为依赖项。

Once this is ready, we can download and unzip it somewhere and we are ready to build our application.

一旦准备好了,我们就可以在某个地方下载并解压缩它,我们就可以准备建立我们的应用程序。

Before moving on, we also need to configure our Cassandra credentials. These all go into src/main/resources/application.properties as taken from the Astra dashboard:

在继续前进之前,我们还需要配置我们的Cassandra凭证。这些都进入src/main/resources/application.properties,这些都来自Astra仪表盘。

ASTRA_DB_ID=e26d52c6-fb2d-4951-b606-4ea11f7309ba
ASTRA_DB_REGION=us-east-1
ASTRA_DB_KEYSPACE=avengers
ASTRA_DB_APPLICATION_TOKEN=AstraCS:xxx-token-here

These secrets are being managed like this purely for the purposes of this article. In a real application, they should be managed securely, for example using Vault.

这些秘密被这样管理纯粹是为了本文的目的。在真正的应用中,它们应该被安全地管理,例如使用Vault

6. Writing a Document Client

6.编写文档客户端

In order to interact with Astra, we need a client that can make the API calls necessary. This will work directly in terms of the Document API that Astra exposes, allowing our application to work in terms of rich documents. For our purposes here, we need to be able to fetch a single record by ID and to provide partial updates to the record.

为了与 Astra 进行交互,我们需要一个能够进行必要的 API 调用的客户端。这将直接在 Astra 公开的 Document API 方面发挥作用,使我们的应用程序能够在丰富的文档方面发挥作用。就我们的目的而言,我们需要能够通过ID来获取一条记录,并对该记录提供部分更新。

In order to manage this, we will write a DocumentClient bean that encapsulates all of this:

为了管理这一点,我们将编写一个DocumentClientbean,将所有这些都封装起来。

@Repository
public class DocumentClient {
  @Value("https://${ASTRA_DB_ID}-${ASTRA_DB_REGION}.apps.astra.datastax.com/api/rest/v2/namespaces/${ASTRA_DB_KEYSPACE}")
  private String baseUrl;

  @Value("${ASTRA_DB_APPLICATION_TOKEN}")
  private String token;

  @Autowired
  private ObjectMapper objectMapper;

  private RestTemplate restTemplate;

  public DocumentClient() {
    this.restTemplate = new RestTemplate();
    this.restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
  }

  public <T> T getDocument(String collection, String id, Class<T> cls) {
    var uri = UriComponentsBuilder.fromHttpUrl(baseUrl)
      .pathSegment("collections", collection, id)
      .build()
      .toUri();
    var request = RequestEntity.get(uri)
      .header("X-Cassandra-Token", token)
      .build();
    var response = restTemplate.exchange(request, cls);
    return response.getBody();
  }

  public void patchSubDocument(String collection, String id, String key, Map<String, Object> updates) {
    var updateUri = UriComponentsBuilder.fromHttpUrl(baseUrl)
      .pathSegment("collections", collection, id, key)
      .build()
      .toUri();
    var updateRequest = RequestEntity.patch(updateUri)
      .header("X-Cassandra-Token", token)
      .body(updates);
    restTemplate.exchange(updateRequest, Map.class);
  } 
}

Here, our baseUrl and token fields are configured from the properties that we defined earlier. We then have a getDocument() method that can call Astra to get the specified record from the desired collection, and a patchSubDocument() method that can call Astra to patch part of any single document in the collection.

在这里,我们的baseUrltoken字段是由我们之前定义的属性配置的。然后我们有一个getDocument()方法,可以调用Astra从所需的集合中获取指定的记录,还有一个patchSubDocument()方法,可以调用Astra来修补集合中任何单个文档的一部分。

That’s all that’s needed to interact with the Document API from Astra since it works by simply exchanging JSON documents over HTTP.

这就是与Astra的Document API进行交互所需要的全部内容,因为它通过HTTP简单地交换JSON文档来工作。

Note that we need to change the request factory used by our RestTemplate. This is because the default one that is used by Spring doesn’t support the PATCH method on HTTP calls.

注意,我们需要改变我们的RestTemplate所使用的请求工厂。这是因为Spring使用的默认工厂不支持HTTP调用中的PATCH方法。

7. Fetching Avengers Statuses via the Document API

7.通过文档API获取复仇者联盟的状态

Our first requirement is to be able to retrieve the statuses of the members of our team. This is the document from the statuses collection that we mentioned earlier. This will be built on top of the DocumentClient that we wrote earlier.

我们的第一个需求是能够检索我们团队中成员的状态。这就是我们之前提到的statuses集合中的文件。这将建立在我们之前写的DocumentClient之上。

7.1. Retrieving Statuses from Astra

7.1.从Astra检索状态

To represent these, we will need a Record as follows:

为了表示这些,我们将需要一个Record,如下所示。

public record Status(String avenger, 
  String name, 
  String realName, 
  String status, 
  String location) {}

We also need a Record to represent the entire collection of statuses as retrieved from Cassandra:

我们还需要一个Record来代表从Cassandra检索到的整个状态集合。

public record Statuses(Map<String, Status> data) {}

This Statuses class represents the exact same JSON as will be returned by the Document API, and so can be used to receive the data via a RestTemplate and Jackson.

这个Statuses类所代表的JSON与将由Document API返回的JSON完全相同,因此可用于通过RestTemplate和Jackson接收数据。

Then we need a service layer to retrieve the statuses from Cassandra and return them for use:

然后我们需要一个服务层来从Cassandra检索状态并返回使用。

@Service
public class StatusesService {
  @Autowired
  private DocumentClient client;
  
  public List<Status> getStatuses() {
    var collection = client.getDocument("statuses", "latest", Statuses.class);

    var result = new ArrayList<Status>();
    for (var entry : collection.data().entrySet()) {
      var status = entry.getValue();
      result.add(new Status(entry.getKey(), status.name(), status.realName(), status.status(), status.location()));
    }

    return result;
  }  
}

Here, we are using our client to get the record from the “statuses” collection, represented in our Statuses record. Once retrieved we extract only the documents to return back to the caller. Note that we do have to rebuild the Status objects to also contain the IDs since these are actually stored higher up in the document within Astra.

在这里,我们正在使用我们的客户端从 “statuses “集合中获取记录,该集合在我们的Statuses记录中表示。一旦获取,我们只提取文件返回给调用者。请注意,我们必须重建Status对象,使其也包含ID,因为这些ID实际上存储在Astra的文档的更高位置。

7.2. Displaying the Dashboard

7.2.显示仪表板

Now that we have a service layer to retrieve the data, we need to do something with it. This means a controller to handle incoming HTTP requests from the browser, and then render a template showing the actual dashboard.

现在我们有了一个服务层来检索数据,我们需要对其进行处理。这意味着需要一个控制器来处理来自浏览器的HTTP请求,然后渲染一个显示实际仪表板的模板。

First then, the controller:

首先是控制器。

@Controller
public class StatusesController {
  @Autowired
  private StatusesService statusesService;

  @GetMapping("/")
  public ModelAndView getStatuses() {
    var result = new ModelAndView("dashboard");
    result.addObject("statuses", statusesService.getStatuses());

    return result;
  }
}

This retrieves the statuses from Astra and passes them on to a template to render.

这是从Astra中检索状态,并将它们传递给一个模板来渲染。

Our main “dashboard.html” template is then as follows:

我们的主 “dashboard.html “模板如下。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous" />
  <title>Avengers Status Dashboard</title>
</head>
<body>
  <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container-fluid">
      <a class="navbar-brand" href="#">Avengers Status Dashboard</a>
    </div>
  </nav>

  <div class="container-fluid mt-4">
    <div class="row row-cols-4 g-4">
      <div class="col" th:each="data, iterstat: ${statuses}">
        <th:block th:switch="${data.status}">
          <div class="card text-white bg-danger" th:case="DECEASED" th:insert="~{common/status}"></div>
          <div class="card text-dark bg-warning" th:case="INJURED" th:insert="~{common/status}"></div>
          <div class="card text-dark bg-warning" th:case="UNKNOWN" th:insert="~{common/status}"></div>
          <div class="card text-white bg-secondary" th:case="RETIRED" th:insert="~{common/status}"></div>
          <div class="card text-dark bg-light" th:case="*" th:insert="~{common/status}"></div>
        </th:block>
      </div>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
    crossorigin="anonymous"></script>
</body>
</html>

And this makes use of another nested template, under “common/status.html”, to display the status of a single Avenger:

而这就利用了另一个嵌套的模板,在 “common/status.html “下,来显示单个Avenger的状态。

<div class="card-body">
  <h5 class="card-title" th:text="${data.name}"></h5>
  <h6 class="card-subtitle"><span th:if="${data.realName}" th:text="${data.realName}"></span> </h6>
  <p class="card-text"><span th:if="${data.location}">Location: <span th:text="${data.location}"></span></span> </p>
</div>
<div class="card-footer">Status: <span th:text="${data.status}"></span></div>

This makes use of Bootstrap to format up our page, and displays one card for each Avenger, coloured based on the status and displaying the current details of that Avenger:

这利用Bootstrap来格式化我们的页面,并为每个复仇者显示一张卡片,根据状态着色并显示该复仇者的当前细节。

8. Status Updates via the Document API

8.通过文件API的状态更新

We now have the ability to display the current status data of the various Avengers members. What we’re missing is the ability to update them with feedback from the field. This will be a new HTTP controller that can update our document via the Document API to reflect the newest status details.

我们现在有能力显示复仇者联盟各成员的当前状态数据。我们缺少的是通过现场的反馈来更新它们的能力。这将是一个新的HTTP控制器,可以通过文档API更新我们的文档,以反映最新的状态细节。

In the next article, this same controller will record both the latest status into the statuses collection but also the events collection. This will allow us to record the entire history of events for later analysis from the same input stream. As such, the inputs into this controller are going to be the individual events and not the rolled-up statuses.

在下一篇文章中,同一个控制器将把最新的状态记录到statuses集合中,同时也记录到events集合。这将使我们能够记录整个事件的历史,以便以后从同一输入流中进行分析。因此,这个控制器的输入将是单个事件,而不是滚动的状态。

8.1. Updating Statuses in Astra

8.1.在Astra中更新状态

Because we are representing the statuses data as a single document, we only need to update the appropriate portion of it. This uses the patchSubDocument() method of our client, pointing at the correct portion for the identified avenger.

由于我们将状态数据表示为一个单一的文件,我们只需要更新其中适当的部分。这使用了我们客户端的patchSubDocument()方法,指向了已识别的复仇者的正确部分。

We do this with a new method in the StatusesService class that will perform the updates:

我们通过StatusesService类中的一个新方法来实现这一目标,该方法将执行更新。

public void updateStatus(String avenger, String location, String status) throws Exception {
  client.patchSubDocument("statuses", "latest", avenger, 
    Map.of("location", location, "status", status));
}

8.2. API to Update Statuses

8.2.更新状态的API

We now need a controller that can be called in order to trigger these updates. This will be a new RestController endpoint that takes the avengers ID and the latest event details:

我们现在需要一个可以被调用的控制器,以触发这些更新。这将是一个新的RestController端点,接收avengers ID和最新的事件细节。

@RestController
public class UpdateController {
  @Autowired
  private StatusesService statusesService;

  @PostMapping("/update/{avenger}")
  public void update(@PathVariable String avenger, @RequestBody UpdateBody body) throws Exception {
    statusesService.updateStatus(avenger, lookupLocation(body.lat(), body.lng()), getStatus(body.status()));
  }

  private String lookupLocation(Double lat, Double lng) {
    return "New York";
  }

  private String getStatus(Double status) {
    if (status == 0) {
      return "DECEASED";
    } else if (status > 0.9) {
      return "HEALTHY";
    } else {
      return "INJURED";
    }
  }

  private static record UpdateBody(Double lat, Double lng, Double status) {}
}

This allows us to accept requests for a particular Avenger, containing the current latitude, longitude, and status of that Avenger. We then convert these values into status values and pass them on to the StatusesService to update the status record.

这使我们能够接受对某个特定复仇者的请求,其中包含该复仇者的当前经纬度和状态。然后我们将这些值转换为状态值,并将其传递给StatusesService,以更新状态记录。

In a future article, this will be updated to also create a new events record with this data, so that we can track the entire history of events for every Avenger.

在未来的文章中,这将被更新,同时用这些数据创建一个新的事件记录,这样我们就可以跟踪每个复仇者的整个事件历史。

Note that we are not correctly looking up the name of the location to use for the latitude and longitude – it is just hard-coded. There are various options for implementing this but they are out of scope for this article.

请注意,我们没有正确查找用于经纬度的位置名称–它只是硬编码。有多种方案可以实现这一点,但它们不在本文的范围内。

9. Summary

9.总结

Here we have seen how we can leverage the Astra Document API on top of Cassandra to build a dashboard of statuses. Since Astra is serverless, your demo database will scale to zero when unused, so you will not continue to incur usage charges. In our next article, we will instead work with the Row APIs that allow us to work with very large numbers of records in a very easy manner.

在这里我们看到了我们如何在 Cassandra 的基础上利用 Astra Document API 来构建一个状态仪表板。由于 Astra 是无服务器的,你的演示数据库在未使用时将扩展为零,因此你将不会继续产生使用费。在我们的下一篇文章中,我们将转而使用允许我们以非常简单的方式处理非常多的记录的行API。

All of the code from this article can be found over on GitHub.

本文的所有代码都可以在GitHub上找到over