1. 概述

Apache Cassandra 是一个可扩展的 NoSQL 数据库。它提供了无单点故障的持续可用性。此外,Cassandra 能够处理大量数据,性能出色。

在这个教程中,我们将学习如何使用 Spring Data 和 Docker 连接 Cassandra。我们将利用 Spring Data 的仓库抽象来操作 Cassandra 的数据层。我们将探讨如何在 Cassandra 中保存不同的 Java 日期值,并查看这些日期值如何映射到 Cassandra 类型。

2. Spring Data for Cassandra

Spring Data for Apache Cassandra 为 Spring 开发者提供了与 Cassandra 交互的熟悉接口。这个项目将核心 Spring 概念应用于使用 Cassandra 数据存储的解决方案开发。

Spring Data 让我们能够基于常见的 Spring 接口创建仓库,还允许使用 QueryBuilders 来消除学习 Apache Cassandra 查询语言(CQL)的需求。该项目提供了简单的注解,以实现丰富的对象映射。

有两个重要的辅助类:

  • CqlTemplate 处理常见的数据访问操作
  • CassandraTemplate 提供对象映射

该项目与 Spring 核心 JDBC 支持有显著相似之处。

3. 设置测试环境

为了开始,我们需要连接到一个 Cassandra 实例。

请注意,我们也可以连接到基于 Apache Cassandra 构建的云数据库 Astra DB。

这个指南将教你如何连接到 Datastax Astra DB

3.1. Cassandra 容器

让我们使用 Testcontainers 库配置并启动 Cassandra。首先,我们将定义一个 Cassandra 容器并暴露到特定端口:

@Container
public static final CassandraContainer cassandra = (CassandraContainer) new CassandraContainer("cassandra:3.11.2")
    .withExposedPorts(9042);

接下来,我们需要覆盖 Spring Data 所需的测试属性,以便能与 Cassandra 容器建立连接:

TestPropertyValues.of(
    "spring.data.cassandra.keyspace-name=" + KEYSPACE_NAME,
    "spring.data.cassandra.contact-points=" + cassandra.getContainerIpAddress(),
    "spring.data.cassandra.port=" + cassandra.getMappedPort(9042)
).applyTo(configurableApplicationContext.getEnvironment());

最后,在创建任何对象/表之前,我们需要创建一个键空间:

session.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE_NAME + " WITH replication = {'class':'SimpleStrategy','replication_factor':'1'};");

在 Cassandra 中,键空间只是一个数据容器,实际上,它非常类似于关系型数据库中的数据库。

3.2. Cassandra 仓库

Spring Data 的**仓库支持极大地简化了 DAO 的实现**。让我们从创建一个简单的 DAO 开始。

org.springframework.data.cassandra.core.mapping 包提供的 @Table 注解使域对象映射成为可能:

@Table
public class Person {

    @PrimaryKey
    private UUID id;
    private String firstName;
    private String lastName;

    public Person(UUID id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    //getters, setters, equals and hash code

}

接下来,我们将为我们的 DAO 定义一个 Spring Data 仓库,通过扩展 CassandraRepository 接口:

@Repository
public interface PersonRepository extends CassandraRepository<Person, UUID> {}

最后,在开始编写集成测试之前,我们需要定义两个额外的属性:

spring.data.cassandra.schema-action=create_if_not_exists
spring.data.cassandra.local-datacenter=datacenter1

第一个属性将确保 Spring Data 自动为我们创建带有注解的表。

需要注意的是,这个设置不推荐用于生产系统

4. 使用日期值

在现代版本的 Spring Data for Apache Cassandra 中,处理日期值相当直接。Spring Data 会自动确保 Java 日期类型正确地映射到和从 Apache Cassandra 表达式中

4.1. LocalDate 类型

让我们在 Person DAO 中添加一个类型为 LocalDate 的新字段 birthDate

@Test
public void givenValidPersonUsingLocalDate_whenSavingIt_thenDataIsPersisted() {
    UUID personId = UUIDs.timeBased();
    Person newPerson = new Person(personId, "Luka", "Modric");
    newPerson.setBirthDate(LocalDate.of(1985, 9, 9));
    personRepository.save(newPerson);

    List<Person> savedPersons = personRepository.findAllById(List.of(personId));
    assertThat(savedPersons.get(0)).isEqualTo(newPerson);
}

Spring Data 自动将 Java 的 LocalDate 类型转换为 Cassandra 的 date 类型。在 DAO 中保存和从 Cassandra 获取记录后,LocalDate 值保持不变。

4.2. LocalDateTime 类型

让我们在 Person DAO 中添加一个名为 lastVisitedDateLocalDateTime 类型字段:

@Test
public void givenValidPersonUsingLocalDateTime_whenSavingIt_thenDataIsPersisted() {
    UUID personId = UUIDs.timeBased();
    Person newPerson = new Person(personId, "Luka", "Modric");
    newPerson.setLastVisitedDate(LocalDateTime.of(2021, 7, 13, 11, 30));
    personRepository.save(newPerson);

    List<Person> savedPersons = personRepository.findAllById(List.of(personId));
    assertThat(savedPersons.get(0)).isEqualTo(newPerson);
}

Spring Data 自动将 Java 的 LocalDateTime 类型转换为 Cassandra 的 timestamp 类型。在 DAO 中保存和从 Cassandra 获取记录后,LocalDateTime 值保持不变。

4.3. 旧版日期类型

最后,让我们在 Person DAO 中添加一个名为 lastPurchasedDate 的旧版类型 Date 的字段:

@Test
public void givenValidPersonUsingLegacyDate_whenSavingIt_thenDataIsPersisted() {
    UUID personId = UUIDs.timeBased();
    Person newPerson = new Person(personId, "Luka", "Modric");
    newPerson.setLastPurchasedDate(new Date(LocalDate.of(2021, 7, 13).toEpochDay()));
    personRepository.save(newPerson);

    List<Person> savedPersons = personRepository.findAllById(List.of(personId));
    assertThat(savedPersons.get(0)).isEqualTo(newPerson);
}

LocalDateTime 类似,Spring Data 将 Java 的 java.util.Date 类型转换为 Cassandra 的 timestamp 类型

4.4. 映射的 Cassandra 类型

让我们使用 CQLSH 检查保存在 Cassandra 中的数据。CQLSH 是一个通过 CQL 与 Cassandra 交互的命令行shell

为了检查在测试执行期间存储在 Cassandra 容器中的数据,我们可以在测试中设置断点。然后,在测试暂停时,我们可以通过 Docker Desktop 应用程序连接到 Docker 容器的 CLI:

container 列表

连接到 Docker 容器 CLI 后,我们应该首先选择键空间,然后选择表:

# cqlsh
Connected to Test Cluster at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.2 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh> USE test;
cqlsh:test> select * from person;

因此,CQLSH 将显示保存在表中的数据格式化输出:

 id                                   | birthdate  | firstname | lastname | lastpurchaseddate | lastvisiteddate
--------------------------------------+------------+-----------+----------+-------------------+-----------------
 9abef910-e3fd-11eb-9829-c5149ac796de | 1985-09-09 |      Luka |   Modric |              null |            null

然而,我们也想检查特定日期列使用的数据类型:

cqlsh:test> DESC TABLE person;

输出返回了一个创建表的 CQL 命令,因此包含了所有数据类型定义:

CREATE TABLE test.person (
    id uuid PRIMARY KEY,
    birthdate date,
    firstname text,
    lastname text,
    lastpurchaseddate timestamp,
    lastvisiteddate timestamp
)

5. 结论

在这篇文章中,我们探索了在 Spring Data for Apache Cassandra 中使用不同日期值的方法。

在示例中,我们涵盖了使用 LocalDateLocalDateTime 和旧版 Date Java 类型。我们了解了如何使用 Testcontainers 启动的 Cassandra 实例进行连接。最后,我们利用 Spring Data 仓库抽象来操作存储在 Cassandra 中的数据。

如往常一样,源代码可以在GitHub 上找到