1. 序章

この記事では、チームのメンバーのステータスを監視するためにアベンジャーズが使用する「トニースタークのアベンジャーズステータスダッシュボード」を作成します。

これは、Stargateを使用してApacheCassandraを搭載したDBaaSであるDataStaxAstraを使用して構築され、追加のAPIを提供します。さらに、Spring Bootアプリケーションを使用してダッシュボードと何が起こっているかを示します。

これはJava16でビルドするので、続行する前に、これがインストールされ、使用できる状態になっていることを確認してください。

2. アストラとは何ですか?

DataStax Astraは、ApacheCassandraを利用したサービスとしてのデータベースです。 これにより、データの保存に使用できる完全にホストされ、完全に管理されたCassandraデータベースが提供されます。これには、Cassandraが拡張性、高可用性、およびパフォーマンスのために提供するすべての機能が含まれます。

これに加えて、Astraには、異なるAPIを介してまったく同じ基になるデータを公開するStargateデータプラットフォームも組み込まれています。 これにより、RESTおよびGraphQL APIを使用して従来のCassandraテーブルにアクセスできます。どちらも互いに100% c互換性があり、従来のCQLAPIと互換性があります。 これらにより、Spring RestTemplateなどの標準のHTTPクライアントのみを使用してデータに非常に柔軟にアクセスできます。

また、はるかに柔軟なデータアクセスを可能にするJSONドキュメントAPIも提供します。 このAPIを使用すると、スキーマは不要であり、必要に応じてすべてのレコードを異なる形状にすることができます。 さらに、レコードは必要に応じて複雑にすることができ、データを表現するためのJSONの全機能をサポートします。

ただし、これにはコストが伴います。ドキュメントAPIは他のAPIと互換性がないため、データをモデル化する方法と、データへのアクセスに最適なAPIを事前に決定することが重要です。

3. アプリケーションデータモデル

カサンドラの上にあるアストラシステムを中心にシステムを構築しています。 これは、データをモデル化する方法に直接反映されます。

Cassandraは、非常に高いスループットで大量のデータを許可するように設計されており、レコードを表形式で保存します。 Astraは、これにいくつかの代替API(RESTとGraphQL)と、DocumentAPIを使用してドキュメントと単純な表形式データを表現する機能を追加します。

これは、スキーマ設計を異なる方法で行うCassandraによって引き続きサポートされています。 最新のシステムでは、スペースはもはや制約ではありません。 データの複製は問題にならず、データのコレクションまたはパーティション間で結合する必要がなくなります。 これは、コレクション内のデータをニーズに合わせて非正規化できることを意味します。

そのため、データモデルは、イベントとステータスの2つのコレクションを中心に構築されます。 events コレクションは、これまでに発生したすべてのステータスイベントの記録です。非常に大きく、カサンドラが理想的なものです。 これについては、次の記事で詳しく説明します。

このコレクションのレコードは次のようになります。

復讐者 ファルコン
タイムスタンプ 2021-04-02T14:23:12Z
緯度 40.714558
経度 -73.975029
状態 0.72

これにより、単一のイベント更新が提供され、更新の正確なタイムスタンプと場所、およびアベンジャーのステータスのパーセンテージ値が提供されます。

statuses コレクションには、ダッシュボードデータを含む単一のドキュメントが含まれています。これは、イベントコレクションに入るデータの非正規化された要約ビューです。 このドキュメントは次のようになります。

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

ここに、変更されない一般的なデータ(nameおよびrealNameフィールド)と、このAvengerの最新のイベントから生成された要約データ()があります。 ]locationlatitudeおよびlongitude値から導出され、statusstatusフィールドの一般的な要約です。イベント。

この記事では、 statuses コレクションに焦点を当て、DocumentAPIを使用してコレクションにアクセスします。 次の記事では、代わりに行ベースのデータであるeventsコレクションを操作する方法を示します。

4. DataStaxAstraを設定する方法

アプリケーションを開始する前に、データ用のストアが必要です。 DataStaxAstraのCassandraオファリングを使用します。 開始するには、無料のアカウントをAstraに登録し、新しいデータベースを作成する必要があります。これには、データベースと次のキースペースの両方に適切な名前を付ける必要があります。

(注–画面は公開時点では正確ですが、その後変更されている可能性があります)

設定には数分かかります。 これが完了したら、アクセストークンを作成する必要があります。

これを行うには、新しく作成されたデータベースの[設定]タブにアクセスして、トークンを生成する必要があります。

これらすべてが完了したら、データベースの詳細も必要になります。 これも:

  • データベースID
  • 領域
  • キースペース

これらは「接続」タブにあります。

最後に、いくつかのデータが必要です。 この記事では、事前に入力されたデータを使用しています。 これはシェルスクリプトここにあります。

5. スプリングブーツのセットアップ方法

SpringInitializrを使用して新しいアプリケーションを作成します。 Java 16も使用します–レコードを使用できるようにします。これは、Spring Boot 2.5が必要であることを意味します–現在これは2.5.0-M3を意味します。

さらに、依存関係としてSpringWebとThymeleafが必要です。

これが準備できたら、どこかでダウンロードして解凍し、アプリケーションを構築する準備が整います。

先に進む前に、Cassandraの資格情報も構成する必要があります。 これらはすべて、Astraダッシュボードから取得した src / main / resources /application.propertiesに移動します。

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

これらの秘密は、純粋にこの記事の目的のためにこのように管理されています。 実際のアプリケーションでは、たとえばVaultを使用して安全に管理する必要があります。

6. ドキュメントクライアントの作成

Astraと対話するには、必要なAPI呼び出しを行うことができるクライアントが必要です。これは、Astraが公開する Document API に関して直接機能し、アプリケーションが機能できるようにします。豊富なドキュメントの観点から。 ここでの目的のために、IDで単一のレコードをフェッチし、レコードに部分的な更新を提供できる必要があります。

これを管理するために、これらすべてをカプセル化する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);
  } 
}

ここで、baseUrlおよびtokenフィールドは、前に定義したプロパティから構成されます。 次に、Astraを呼び出して目的のコレクションから指定されたレコードを取得できる getDocument()メソッドと、Astraを呼び出して任意の単一の一部にパッチを適用できる patchSubDocument()メソッドがあります。コレクション内のドキュメント。

AstraのDocumentAPIとやり取りするために必要なのはこれだけです。これは、HTTPを介してJSONドキュメントを交換するだけで機能するためです。

RestTemplateで使用されるリクエストファクトリを変更する必要があることに注意してください。 これは、Springで使用されるデフォルトのメソッドがHTTP呼び出しでPATCHメソッドをサポートしていないためです。

7. DocumentAPIを介したアベンジャーズステータスの取得

最初の要件は、チームのメンバーのステータスを取得できるようにすることです。 これは、前述のステータスコレクションのドキュメントです。 これは、 DocumentClient 以前に書いたこと。

7.1. アストラからステータスを取得する

これらを表すには、次のようにRecordが必要になります。

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

また、Cassandraから取得したステータスのコレクション全体を表すレコードも必要です。

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

このStatusesクラスは、Document APIによって返されるものとまったく同じJSONを表すため、RestTemplateおよびJacksonを介してデータを受信するために使用できます。

次に、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;
  }  
}

ここでは、クライアントを使用して、Statusesレコードに表示される「statuses」コレクションからレコードを取得しています。取得すると、ドキュメントのみを抽出して呼び出し元に返します。 Status オブジェクトを再構築してIDも含める必要があることに注意してください。これは、IDが実際にはAstra内のドキュメントの上位に格納されているためです。

7.2. ダッシュボードの表示

データを取得するためのサービスレイヤーができたので、それを使って何かを行う必要があります。これは、ブラウザーからの着信HTTPリクエストを処理し、実際のダッシュボードを示すテンプレートをレンダリングするコントローラーを意味します。

まず、コントローラー:

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

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

    return result;
  }
}

これにより、Astraからステータスが取得され、それらがテンプレートに渡されてレンダリングされます。

メインの「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/[email protected]/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/[email protected]/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
    crossorigin="anonymous"></script>
</body>
</html>

そして、これは「common / status.html」の下にある別のネストされたテンプレートを利用して、単一のアベンジャーのステータスを表示します。

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

これは、 Bootstrap を使用してページをフォーマットし、ステータスに基づいて色分けされ、そのAvengerの現在の詳細を表示するAvengerごとに1枚のカードを表示します。

8. ドキュメントAPIを介したステータス更新

これで、さまざまなアベンジャーズメンバーの現在のステータスデータを表示できるようになりました。 私たちに欠けているのは、現場からのフィードバックでそれらを更新する機能です。 これは、最新のステータスの詳細を反映するようにDocumentAPIを介してドキュメントを更新できる新しいHTTPコントローラーになります。

次の記事では、この同じコントローラーが最新のステータスを statuses コレクションだけでなく、eventsコレクションにも記録します。 これにより、同じ入力ストリームから後で分析するために、イベントの履歴全体を記録できます。 そのため、このコントローラーへの入力は、ロールアップされたステータスではなく、個々のイベントになります。

8.1. アストラのステータスの更新

ステータスデータを単一のドキュメントとして表しているため、その適切な部分のみを更新する必要があります。これは、クライアントの patchSubDocument()メソッドを使用し、正しいポイントを指します。識別された復讐者のための部分。

これは、更新を実行する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

これらの更新をトリガーするために呼び出すことができるコントローラーが必要です。これは、アベンジャーズIDと最新のイベントの詳細を取得する新しいRestControllerエンドポイントになります。

@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) {}
}

これにより、特定のアベンジャーの現在の緯度、経度、およびステータスを含む、そのアベンジャーのリクエストを受け入れることができます。 次に、これらの値をステータス値に変換し、 StatusesService に渡して、ステータスレコードを更新します。

将来の記事では、これが更新され、このデータを使用して新しいイベントレコードも作成されるため、すべてのアベンジャーのイベントの履歴全体を追跡できます。

緯度と経度に使用する場所の名前を正しく検索していないことに注意してください。ハードコーディングされているだけです。 これを実装するためのさまざまなオプションがありますが、それらはこの記事の範囲外です。

9. 概要

ここでは、Cassandra上でAstra Document APIを活用して、ステータスのダッシュボードを構築する方法を説明しました。 Astraはサーバーレスであるため、デモデータベースは未使用時にゼロにスケーリングされるため、続行しません。使用料が発生します。 次の記事では、代わりにRow APIを使用して、非常に多数のレコードを非常に簡単に処理できるようにします。

この記事のすべてのコードは、GitHubにあります。