跳到主要内容
版本:5.1.1

持久化实体

Doubao 中英对照 Persisting Entities

CassandraTemplate 类(及其响应式变体 ReactiveCassandraTemplate)位于 org.springframework.data.cassandra 包中,是 Spring 的 Cassandra 支持中的核心类,并提供丰富的功能集来与数据库交互。该模板提供便捷操作,以创建、更新、删除和查询 Cassandra,并在您的领域对象与 Cassandra 表中的行之间提供映射。

备注

模板实例一旦配置完成即是线程安全的,可在多个实例间复用。

Cassandra 中的行与应用领域类之间的映射,通过委托给 CassandraConverter 接口的实现来完成。Spring 提供了默认实现 MappingCassandraConverter,但您也可以编写自己的自定义转换器。有关更详细的信息,请参阅 Cassandra 转换 一节。

CassandraTemplate 类实现了 CassandraOperations 接口,其响应式变体 ReactiveCassandraTemplate 实现了 ReactiveCassandraOperations。在尽可能的情况下,[Reactive]CassandraOperations 上的方法均以 Cassandra 中可用的方法命名,以便让已经熟悉 Cassandra 的开发者对该 API 感到熟悉。

例如,您可以找到 select、insert、delete 和 update 等方法。其设计目标是尽可能轻松地在基础 Cassandra 驱动的使用与 [Reactive]CassandraOperations 的使用之间进行切换。这两个 API 之间的一个主要区别是:CassandraOperations 可以接收领域对象,而不必使用 CQL 和查询对象。

备注

引用 [Reactive]CassandraTemplate 实例上操作的首选方式,是通过 [Reactive]CassandraOperations 接口。

[Reactive]CassandraTemplate 使用的默认转换器实现是 MappingCassandraConverter。虽然 MappingCassandraConverter 可以使用额外的元数据来指定对象到行的映射,但它也可以通过使用一些字段与表名映射的约定,来转换不包含额外元数据的对象。这些约定以及映射注解的使用,将在 "映射" 一章 中说明。

[Reactive]CassandraTemplate 的另一个核心特性,是将 Cassandra Java 驱动中抛出的异常转换为 Spring 可移植的数据访问异常体系。有关更多信息,请参阅 异常转换 一节。

备注

模板 API 具有不同的执行模型风格。基础的 CassandraTemplate 使用阻塞式(命令式同步)执行模型。您可以使用 AsyncCassandraTemplate 进行异步执行并与 CompletableFuture 实例同步,或使用 ReactiveCassandraTemplate 进行响应式执行。

实例化 CassandraTemplate​

CassandraTemplate 应始终配置为 Spring bean,尽管我们在前面展示了一个可以直接实例化它的示例。然而,由于我们假设的是构建 Spring 模块的上下文,因此我们假定 Spring 容器是存在的。

获取 CassandraTemplate 有两种方式,具体取决于您如何加载 Spring ApplicationContext:

自动装配​

您可以将 [Reactive]CassandraOperations 自动装配到您的项目中,如下例所示:

@Autowired
private CassandraOperations cassandraOperations;

与所有 Spring 自动装配一样,这假定 ApplicationContext 中只有一个 [Reactive]CassandraOperations 类型的 bean。如果您有多个 [Reactive]CassandraTemplate bean(当您在同一项目中使用多个键空间时就是这种情况),那么您可以使用 @Qualifier 注解来指定想要自动装配的 bean。

@Autowired
@Qualifier("keyspaceOneTemplateBeanId")
private CassandraOperations cassandraOperations;

通过 ApplicationContext 查找 Bean​

您也可以从 ApplicationContext 中查找 [Reactive]CassandraTemplate bean,如下例所示:

CassandraOperations cassandraOperations = applicationContext.getBean("cassandraTemplate", CassandraOperations.class);

查询行​

您可以使用 Query 和 Criteria 类来表达查询,这两个类的方法名反映了 Cassandra 原生命谓词运算符的名称,例如 lt、lte、is 等。

Query 和 Criteria 类采用流式 API 风格,以便您可以轻松地将多个方法条件和查询串联在一起,同时使代码易于理解。在 Java 中创建 Query 和 Criteria 实例时会使用静态导入,以提高可读性。

查询表中的行​

在前面的章节中,我们了解了如何使用 [Reactive]CassandraTemplate 上的 selectOneById 方法来检索单个对象。这样做会返回单个领域对象。我们也可以查询一组行,并将其作为领域对象列表返回。假设我们有若干 Person 对象,其姓名和年龄值作为行存储在表中,且每个人都有一个账户余额,那么我们现在可以使用以下代码运行查询:

import static org.springframework.data.cassandra.core.query.Criteria.where;
import static org.springframework.data.cassandra.core.query.Query.query;

…

List<Person> result = cassandraTemplate.select(query(where("age").is(50))
.and(where("balance").gt(1000.00d)).withAllowFiltering(), Person.class);

select、selectOne 和 stream 方法接收一个 Query 对象作为参数。该对象定义了用于执行查询的条件和选项。条件通过使用 Criteria 对象来指定,该对象有一个名为 where 的静态工厂方法,用于实例化一个新的 Criteria 对象。我们建议对 org.springframework.data.cassandra.core.query.Criteria.where 和 Query.query 使用静态导入,以使查询更易读。

该查询应返回一个满足指定条件的 Person 对象列表。Criteria 类具有以下与 Apache Cassandra 所提供运算符相对应的方法:

Criteria 类的方法​

  • CriteriaDefinition gt (Object value):使用 > 运算符创建一个条件。

  • CriteriaDefinition gte (Object value):使用 >= 运算符创建一个条件。

  • CriteriaDefinition in (Object…​ values):对可变参数使用 IN 运算符创建一个条件。

  • CriteriaDefinition in (Collection<?> collection):使用集合、通过 IN 运算符创建一个条件。

  • CriteriaDefinition is (Object value):使用字段匹配(column = value)创建一个条件。

  • CriteriaDefinition lt (Object value):使用 < 运算符创建一个条件。

  • CriteriaDefinition lte (Object value):使用 ⇐ 运算符创建一个条件。

  • CriteriaDefinition like (Object value):使用 LIKE 运算符创建一个条件。

  • CriteriaDefinition contains (Object value):使用 CONTAINS 运算符创建一个条件。

  • CriteriaDefinition containsKey (Object key):使用 CONTAINS KEY 运算符创建一个条件。

Criteria 一旦创建即不可变。

Query 类的方法​

Query 类还有一些附加方法,您可以使用它们为查询提供选项:

  • Query by (CriteriaDefinition…​ criteria):用于创建 Query 对象。

  • Query and (CriteriaDefinition criteria):用于向查询添加附加条件。

  • Query columns (Columns columns):用于定义要包含在查询结果中的列。

  • Query limit (Limit limit):用于将返回结果的大小限制为所提供的限制(用于 SELECT 限制)。

  • Query limit (long limit):用于将返回结果的大小限制为所提供的限制(用于 SELECT 限制)。

  • Query pageRequest (Pageable pageRequest):用于将 Sort、PagingState 和 fetchSize 与查询关联起来(用于分页)。

  • Query pagingState (CassandraScrollPosition pagingState):用于将 CassandraScrollPosition 与查询关联起来(用于分页)。

  • Query pagingState (ByteBuffer pagingState):用于将原始 ByteBuffer 与查询关联起来(用于分页)。

  • Query queryOptions (QueryOptions queryOptions):用于将 QueryOptions 与查询关联起来。

  • Query sort (Sort sort):用于为结果提供排序定义。

  • Query withAllowFiltering ():用于生成 ALLOW FILTERING 查询。

Query 一旦创建即不可变。调用方法会创建新的不可变(中间)Query 对象。

查询行的方法​

Query 类具有以下返回行的方法:

  • List<T> select (Query query, Class<T> entityClass):从表中查询一个类型为 T 的对象列表。

  • T selectOne (Query query, Class<T> entityClass):从表中查询一个类型为 T 的对象。

  • Slice<T> slice (Query query, Class<T> entityClass):通过从表中查询一个类型为 T 的对象 Slice,来启动或继续分页。

  • Stream<T> stream (Query query, Class<T> entityClass):从表中查询一个类型为 T 的对象流。

  • List<T> select (String cql, Class<T> entityClass):通过提供一条 CQL 语句,从表中即席查询一个类型为 T 的对象列表。

  • T selectOne (String cql, Class<T> entityClass):通过提供一条 CQL 语句,从表中即席查询一个类型为 T 的对象。

  • Stream<T> stream (String cql, Class<T> entityClass):通过提供一条 CQL 语句,从表中即席查询一个类型为 T 的对象流。

查询方法必须指定要返回的目标类型 T。

列投影​

当未指定 Query.colums(…) 时,会选择所有列(SELECT * FROM …),以便通过 @Value(…) 以及在使用开放投影时,能够访问实体中的所有可用列。

如果您为查询提供了更多信息(例如,通过 流式模板 API 使用封闭 DTO 或接口投影时),那么列的选择将受到限制。

您可以为每个 CassandraTemplate 实例在 StatementFactory 上配置默认的 ProjectionFunction,以影响默认的列选择:

CassandraTemplate template = …;
template.getStatementFactory().setProjectionFunction(ProjectionFunction.mappedProperties());

通过 ProjectionFunctions.mappedProperties() 显式指定列,会将选择限制为实体类型的已映射属性,但其显式特性会在运行查询时根据实体映射对查询进行校验,任何不期望的模式变更都会通过异常显现出来。请注意,任何未映射的属性都不能用于 @Value(…)。

通过 Query.columns(…) 使用显式列选择,会绕过投影函数的 Columns 计算。

流式模板 API​

[Reactive]CassandraOperations 接口是与 Apache Cassandra 进行更底层交互时的核心组件之一。它提供了广泛的方法。每个方法都有多个重载。其中大多数涵盖了 API 的可选(可空)部分。

FluentCassandraOperations 及其响应式变体 ReactiveFluentCassandraOperations,为 [Reactive]CassandraOperations 的常用方法提供了一个更窄的接口,从而提供更可读的流式 API。入口点(query(…)、insert(…)、update(…) 和 delete(…))基于要执行的操作遵循自然的命名方案。从入口点继续,该 API 被设计为仅提供依赖于上下文的方法,引导开发者走向一个终止方法,由该终止方法调用实际的 [Reactive]CassandraOperations。以下示例展示了该流式 API:

List<SWCharacter> all = ops.query(SWCharacter.class)
.inTable("star_wars") 1
.all();
  • Skip this step if SWCharacter defines the table name with @Table or if using the class name as the table name is not a problem

如果 Cassandra 中的一个表保存了不同类型的实体,例如在一个 SWCharacters 表中保存了一个 Jedi,那么您可以使用不同的类型来映射查询结果。您可以使用 as(Class<?> targetType) 将结果映射到不同的目标类型,而 query(Class<?> entityType) 仍然应用于查询和表名。以下示例使用了 query 和 as 方法:

List<Jedi> all = ops.query(SWCharacter.class)    1
.as(Jedi.class) 2
.matching(query(where("jedi").is(true)))
.all();
  • The query fields are mapped against the SWCharacter type.

  • Resulting rows are mapped into Jedi.

提示

您可以通过 as(Class<?>) 仅提供 interface 类型,从而直接将投影应用到结果文档上。

终止方法(first()、one()、all() 和 stream())负责在检索单个实体与将多个实体检索为 List 或 Stream 以及类似操作之间进行切换。

注意

新的流式模板 API 方法(即 query(…)、insert(…)、update(…) 和 delete(…))实际上使用线程安全的支持对象来组装 CQL 语句。然而,这会带来额外的新生代 JVM 堆开销,因为该设计基于用 final 字段来表示各个 CQL 语句组件,并在变更时进行构造。当您可能插入或删除大量对象时(例如在循环内部),应当小心。

向量搜索查询​

投影是以不同于实体的形式查询返回数据的基础。虽然 Cassandra 的主要用例遵循键值模型——存储您所检索的内容——但向量搜索则不同。运行向量搜索查询通常会产生聚合或报表式的结果集。一个典型的查询会返回某种形式的内容(例如一个 text 列),以及它与实际向量之间的评分(或距离)。

考虑以下领域模型:

示例 1. 向量搜索模型

@Table
class Comments {

@Id UUID id;
String comment;

@VectorType(dimensions = 5)
@SaiIndexed Vector vector;
}

class CommentSearch {

String comment;

float similarity;
}

Comments 是定义了一个向量列和 comment 列的领域类型。

运行向量搜索需要使用 ANN 排序,并且通常会定义一个相似度函数,以确定它与给定向量之间的距离。

示例 2. 使用向量搜索

Columns columns = Columns.from("comment")                                                1
.select("vector", builder -> builder.similarity(vector)
.cosine().as("similarity")); 2

Query query = Query.select(columns)
.limit(3)
.sort(VectorSort.ann("vector", vector)); 3

template.query(Comments.class)
.as(CommentSearch.class) 4
.matching(query)
.all();
  • 选择要查询的列。

  • 包含对 similarity_cosine(vector, […]) 的函数调用。Columns.select(…) 使用选择器构建器定制器来配置实际的选择。请务必声明一个别名,以便将结果映射到 CommentSearch.similarity,因为结果映射使用列名。

  • 使用 VectorSort.ann(…) 来定义相对于给定向量的排序顺序。

  • 定义要将结果映射到的目标类型。CommentSearch 类型是结果投影类型,定义了映射结果的列。

保存、更新和删除行​

[Reactive]CassandraTemplate 为您提供了一种简单的方式来保存、更新和删除您的领域对象,并将这些对象映射到 Cassandra 中管理的表。

类型映射​

Spring Data for Apache Cassandra 依赖 DataStax Java 驱动的 CodecRegistry 来确保类型支持。随着类型的添加或变更,Spring Data for Apache Cassandra 模块无需修改即可继续运行。有关当前的类型映射矩阵,请参阅 CQL 数据类型 和 "数据映射与类型转换"。

插入和更新行的方法​

[Reactive]CassandraTemplate 有几个便捷方法来保存和插入您的对象。为了对转换过程进行更细粒度的控制,您可以向 MappingCassandraConverter 注册 Spring Converter 实例(例如 Converter<Row, Person>)。

备注

插入操作与更新操作之间的区别在于:INSERT 操作不会插入 null 值。

使用 INSERT 操作的简单情形是保存一个 POJO。在这种情况下,表名由简单类名(而非全限定类名)确定。存储该对象的表可以通过使用映射元数据来覆盖。

在插入或更新时,必须设置 id 属性。Apache Cassandra 没有生成 ID 的手段。

以下示例使用保存操作并检索其内容:

import static org.springframework.data.cassandra.core.query.Criteria.where;
import static org.springframework.data.cassandra.core.query.Query.query;
…

Person bob = new Person("Bob", 33);
cassandraTemplate.insert(bob);

Person queriedBob = cassandraTemplate.selectOneById(query(where("age").is(33)), Person.class);

您可以使用以下操作来插入和保存:

  • void insert (Object objectToSave):将对象插入 Apache Cassandra 表中。

  • WriteResult insert (Object objectToSave, InsertOptions options):将对象插入 Apache Cassandra 表中并应用 InsertOptions。

您可以使用以下更新操作:

  • void update (Object objectToSave):更新 Apache Cassandra 表中的对象。

  • WriteResult update (Object objectToSave, UpdateOptions options):更新 Apache Cassandra 表中的对象并应用 UpdateOptions。

您也可以使用传统方式,自己编写 CQL 语句,如下例所示:

String cql = "INSERT INTO person (age, name) VALUES (39, 'Bob')";

cassandraTemplate().getCqlOperations().execute(cql);

在使用 InsertOptions 和 UpdateOptions 时,您还可以配置其他选项,例如 TTL、一致性级别和轻量事务(LWT)。

我的行被插入到哪个表?​

您可以通过两种方式来管理用于操作表的表名。默认表名是将简单类名更改为以小写字母开头。因此,com.example.Person 类的实例会存储在 person 表中。第二种方式是在 @Table 注解中指定表名。

在批量中插入、更新和删除单个对象​

Cassandra 协议支持通过使用批量(batch)在一次操作中插入一组行。

[Reactive]CassandraTemplate 接口中的以下方法支持此功能:

  • batchOps:创建一个新的 [Reactive]CassandraBatchOperations 来填充该批量。

[Reactive]CassandraBatchOperations

  • insert:接收单个对象、一个数组(可变参数)或一个对象 Iterable 进行插入。

  • update:接收单个对象、一个数组(可变参数)或一个对象 Iterable 进行更新。

  • delete:接收单个对象、一个数组(可变参数)或一个对象 Iterable 进行删除。

  • withTimestamp:为该批量应用一个 TTL。

  • execute:执行该批量。

更新表中的行​

对于更新,您可以选择更新若干行。

以下示例展示了如何通过使用 + 赋值向余额添加一次性 $50.00 奖金,来更新单个账户对象:

import static org.springframework.data.cassandra.core.query.Criteria.where;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.cassandra.core.query.Update;

…

boolean applied = cassandraTemplate.update(Query.query(where("id").is("foo")),
Update.create().increment("balance", 50.00), Account.class);

除了前面讨论的 Query 之外,我们还通过使用 Update 对象来提供更新定义。Update 类具有与 Apache Cassandra 可用的更新赋值相匹配的方法。

大多数方法会返回 Update 对象,以便为代码风格提供流式 API。

执行行更新的方法​

更新方法可以更新行,如下所示:

  • boolean update (Query query, Update update, Class<?> entityClass):更新 Apache Cassandra 表中所选的一组对象。

Update 类的方法​

Update 类可以配合一点“语法糖”来使用,因为其方法本就意在串联在一起。此外,您还可以通过静态方法 public static Update update(String key, Object value) 并使用静态导入,来快速创建一个新的 Update 实例。

Update 类具有以下方法:

  • AddToBuilder addTo (String columnName) AddToBuilder 入口点:

    • Update prepend(Object value):通过使用 + 更新赋值,将一个集合值前置到现有集合。

    • Update prependAll(Object…​ values):通过使用 + 更新赋值,将所有集合值前置到现有集合。

    • Update append(Object value):通过使用 + 更新赋值,将一个集合值追加到现有集合。

    • Update append(Object…​ values):通过使用 + 更新赋值,将所有集合值追加到现有集合。

    • Update entry(Object key, Object value):通过使用 + 更新赋值添加一个 map 条目。

    • Update addAll(Map<? extends Object, ? extends Object> map):通过使用 + 更新赋值将所有 map 条目添加到 map 中。

  • Update remove (String columnName, Object value):通过使用 - 更新赋值从集合中移除该值。

  • Update clear (String columnName):清空集合。

  • Update increment (String columnName, Number delta):通过使用 + 更新赋值进行更新。

  • Update decrement (String columnName, Number delta):通过使用 - 更新赋值进行更新。

  • Update set (String columnName, Object value):通过使用 = 更新赋值进行更新。

  • SetBuilder set (String columnName) SetBuilder 入口点:

    • Update atIndex(int index).to(Object value):使用 = 更新赋值,将给定索引处的集合设置为一个值。

    • Update atKey(String object).to(Object value):使用 = 更新赋值,将给定键处的 map 条目设置为一个值。

以下清单展示了几个更新示例:

// UPDATE … SET key = 'Spring Data';
Update.update("key", "Spring Data")

// UPDATE … SET key[5] = 'Spring Data';
Update.empty().set("key").atIndex(5).to("Spring Data");

// UPDATE … SET key = key + ['Spring', 'DATA'];
Update.empty().addTo("key").appendAll("Spring", "Data");

请注意,Update 一旦创建即不可变。调用方法会创建新的不可变(中间)Update 对象。

删除行的方法​

您可以使用以下重载方法从数据库中移除对象:

  • boolean delete (Query query, Class<?> entityClass):删除由 Query 选中的对象。

  • T delete (T entity):删除给定对象。

  • T delete (T entity, QueryOptions queryOptions):应用 QueryOptions 删除给定对象。

  • boolean deleteById (Object id, Class<?> entityClass):使用给定 Id 删除对象。

乐观锁定​

@Version 注解在 Cassandra 的上下文中提供了与 JPA 类似的语法,并确保更新仅应用于版本匹配的行。乐观锁定利用 Cassandra 的轻量事务(LWT)来有条件地插入、更新和删除行。因此,INSERT 语句会带 IF NOT EXISTS 条件执行。对于更新和删除,版本属性的实际值会被添加到 UPDATE 条件中,使得如果在此期间有另一个操作更改了该行,则该修改不会产生任何效果。在这种情况下,会抛出 OptimisticLockingFailureException。以下示例展示了这些特性:

@Table
class Person {

@Id String id;
String firstname;
String lastname;
@Version Long version;
}

Person daenerys = template.insert(new Person("Daenerys")); 1

Person tmp = template.findOne(query(where("id").is(daenerys.getId())), Person.class); 2

daenerys.setLastname("Targaryen");
template.save(daenerys); 3

template.save(tmp); // throws OptimisticLockingFailureException // <4>
  • 初始插入文档。version 被设置为 0。

  • 加载刚插入的文档。version 仍为 0。

  • 更新 version = 0 的文档。设置 lastname 并将 version 提升为 1。

  • 尝试更新先前加载的、仍为 version = 0 的文档。由于当前 version 为 1,该操作会因 OptimisticLockingFailureException 而失败。

备注

乐观锁定仅支持单实体操作,不支持批量操作。