1. Введение
В этой статье мы собираемся создать «Панель состояния Мстителей Тони Старка», которую Мстители используют для отслеживания статуса членов команды.
Это будет построено с использованием DataStax Astra , DBaaS на базе Apache Cassandra с использованием Stargate , чтобы предложить дополнительные API для работы с ним. Кроме того, мы будем использовать приложение Spring Boot для отображения приборной панели и демонстрации того, что происходит.
Мы будем создавать это с помощью Java 16, поэтому убедитесь, что он установлен и готов к использованию, прежде чем продолжить.
2. Что такое Астра?
DataStax Astra — это база данных как услуга, работающая на базе Apache Cassandra. Это дает нам полностью размещенную, полностью управляемую базу данных Cassandra, которую мы можем использовать для хранения наших данных, которая включает в себя все возможности, которые Cassandra предлагает для масштабируемости, высокой доступности и производительности.
Кроме того, Astra также включает платформу данных Stargate, которая предоставляет одни и те же базовые данные через разные API. Это дает нам доступ к традиционным таблицам Cassandra с использованием API-интерфейсов REST и GraphQL, которые на 100 % совместимы друг с другом и с более традиционными API-интерфейсами CQL. Это может сделать доступ к нашим данным невероятно гибким с помощью только стандартного HTTP-клиента, такого как Spring RestTemplate
.
Он также предлагает API-интерфейс JSON Document, который обеспечивает гораздо более гибкий доступ к данным. С этим API нет необходимости в схеме, и каждая запись может иметь различную форму, если это необходимо. Кроме того, записи могут быть настолько сложными, насколько это необходимо, поддерживая всю мощь JSON для представления данных.
Однако за это приходится платить — Document API не является взаимозаменяемым с другими API, поэтому важно заранее решить, как следует моделировать данные и какие API лучше всего использовать для доступа к ним.
3. Модель данных нашего приложения
Мы строим нашу систему вокруг системы Astra поверх Cassandra. Это напрямую отразится на том, как мы моделируем наши данные.
Cassandra предназначена для обработки больших объемов данных с очень высокой пропускной способностью и хранит записи в табличной форме. Astra добавляет к этому несколько альтернативных API — REST и GraphQL — и возможность представлять документы, а также простые табличные данные — с помощью Document API.
Это по-прежнему поддерживается Cassandra, которая по-другому разрабатывает схему. В современных системах пространство больше не является ограничением. Дублирование данных перестает быть проблемой, устраняя необходимость в объединении коллекций или разделов данных. Это означает, что мы можем денормализовать наши данные в наших коллекциях в соответствии с нашими потребностями.
Таким образом, наша модель данных будет построена вокруг двух коллекций — событий
и статусов
. Коллекция событий
— это запись каждого события состояния, которое когда-либо происходило — она потенциально может стать очень большой, для чего Cassandra идеально подходит. Подробнее об этом будет рассказано в следующей статье.
Записи в этой коллекции будут выглядеть следующим образом:
| мститель | сокол |
| отметка времени | 2021-04-02T14:23:12Z |
| широта | 40.714558 |
| долгота | -73,975029 |
| статус | 0,72 |
Это дает нам одно обновление события, указывающее точную отметку времени и место обновления, а также процентное значение для статуса Мстителя.
Коллекция статусов
содержит один документ, содержащий данные информационной панели, которая представляет собой денормализованное сводное представление данных, которые входят в коллекцию событий
. Этот документ будет выглядеть примерно так:
{
"falcon": {
"realName": "Sam Wilson",
"location": "New York",
"status": "INJURED",
"name": "Falcon"
},
"wanda": {
"realName": "Wanda Maximoff",
"location": "New York",
"status": "HEALTHY"
}
}
Здесь у нас есть некоторые общие данные, которые не меняются — поля имени
и реального имени
— и у нас есть некоторые сводные данные, которые сгенерированы из самого последнего события для этого Мстителя — местоположение
получено из значений широты
и долготы , а
статус
— это общая сводка поля статуса
из события.
Эта статья посвящена коллекции статусов
и доступу к ней с помощью Document API. В нашей следующей статье будет показано, как работать с коллекцией событий
, которая представляет собой данные на основе строк.
4. Как настроить DataStax Astra
Прежде чем мы сможем запустить наше приложение, нам нужно хранилище для наших данных. Мы собираемся использовать предложение Cassandra от DataStax Astra. Для начала нам нужно зарегистрировать бесплатную учетную запись в Astra и создать новую базу данных. Этому необходимо дать разумное имя как для базы данных, так и для пространства ключей внутри:
(Примечание: скрины точны на момент публикации, но с тех пор могли измениться)
Настройка займет несколько минут. Как только это будет сделано, нам нужно будет создать токен доступа.
Для этого нам нужно перейти на вкладку «Настройки» для вновь созданной базы данных и сгенерировать токен:
Как только все это будет сделано, нам также понадобятся данные нашей базы данных. Это включает:
- Идентификатор базы данных
- Область, край
- Ключевое пространство
Их можно найти на вкладке «Подключение».
Наконец, нам нужны некоторые данные. Для целей этой статьи мы используем некоторые предварительно заполненные данные. Это можно найти в сценарии оболочки здесь .
5. Как настроить загрузку Spring
Мы собираемся создать наше новое приложение, используя Spring Initializr ; мы также собираемся использовать Java 16, что позволит нам использовать Records . Это, в свою очередь, означает, что нам нужен Spring Boot 2.5 — в настоящее время это означает 2.5.0-M3.
Кроме того, нам нужны Spring Web и Thymeleaf в качестве зависимостей:
Как только это будет готово, мы можем загрузить и разархивировать его куда-нибудь, и мы готовы создать наше приложение.
Прежде чем двигаться дальше, нам также необходимо настроить учетные данные 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
Эти секреты управляются таким образом исключительно для целей этой статьи. В реальном приложении ими следует управлять безопасно, например, с помощью Vault .
6. Написание клиента документов
Чтобы взаимодействовать с Astra, нам нужен клиент, который может сделать необходимые вызовы API. Это будет работать непосредственно с точки зрения Document API , предоставляемого Astra, что позволит нашему приложению работать с многофункциональными документами. Для наших целей нам нужно иметь возможность получить одну запись по идентификатору и предоставить частичные обновления записи.
Чтобы справиться с этим, мы напишем bean-компонент DocumentClient
, который инкапсулирует все это:
@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
настраиваются из свойств, которые мы определили ранее. Затем у нас есть метод getDocument()
, который может вызывать Astra для получения указанной записи из нужной коллекции, и метод patchSubDocument()
, который может вызывать Astra для исправления части любого отдельного документа в коллекции.
Это все, что нужно для взаимодействия с Document API от Astra, поскольку он работает, просто обмениваясь документами JSON через HTTP.
Обратите внимание, что нам нужно изменить фабрику запросов, используемую нашим RestTemplate
. Это связано с тем, что метод по умолчанию, используемый Spring, не поддерживает метод PATCH для HTTP-вызовов.
7. Получение статусов Мстителей через Document API
Наше первое требование — иметь возможность получать статусы членов нашей команды. Это документ из коллекции статусов
, о которой мы упоминали ранее. Это будет построено поверх DocumentClient
, который мы написали ранее.
7.1. Получение статусов из Astra
Чтобы представить их, нам понадобится запись следующим образом:
public record Status(String avenger,
String name,
String realName,
String status,
String location) {}
Нам также нужна запись для представления всей коллекции статусов, полученных от Cassandra:
public record Statuses(Map<String, Status> data) {}
Этот класс Statuses
представляет точно такой же JSON, который будет возвращен Document API, и поэтому может использоваться для получения данных через 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;
}
}
Здесь мы используем наш клиент для получения записи из коллекции «статусы», представленной в нашей записи « Статусы
» . После извлечения мы извлекаем только те документы, которые возвращаются обратно вызывающей стороне. Обратите внимание, что нам нужно перестроить объекты состояния
, чтобы они также содержали идентификаторы, поскольку они фактически хранятся выше в документе в 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/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>
И это использует другой вложенный шаблон в «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 для форматирования нашей страницы и отображает одну карточку для каждого Мстителя, окрашенную в зависимости от статуса и отображающую текущие сведения об этом Мстителе:
8. Обновления статуса через Document API
Теперь у нас есть возможность отображать текущие данные о статусе различных членов Мстителей. Чего нам не хватает, так это возможности обновлять их с учетом отзывов с мест. Это будет новый HTTP-контроллер, который может обновлять наш документ через Document API, чтобы отображать новейшие сведения о статусе.
В следующей статье этот же контроллер будет записывать как последний статус в коллекцию статусов , так и коллекцию
событий
. Это позволит нам записывать всю историю событий для последующего анализа из того же входного потока. Таким образом, входными данными для этого контроллера будут отдельные события, а не свернутые состояния.
8.1. Обновление статусов в Astra
Поскольку мы представляем данные статусов в виде единого документа, нам нужно обновить только соответствующую его часть. Здесь используется метод 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 для обновления статусов
Теперь нам нужен контроллер, который можно вызывать для запуска этих обновлений. Это будет новая конечная точка 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. Резюме
Здесь мы увидели, как мы можем использовать API Astra Document поверх Cassandra для создания панели статусов. Поскольку Astra является бессерверной, ваша демонстрационная база данных будет масштабироваться до нуля, когда она не будет использоваться, поэтому с вас больше не будет взиматься плата за использование. Вместо этого в нашей следующей статье мы будем работать с Row API, которые позволяют нам очень легко работать с очень большим количеством записей.
Весь код из этой статьи можно найти на GitHub .