持久化实体
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 自动装配到您的项目中,如下例所示:
- Imperative
- Reactive
@Autowired
private CassandraOperations cassandraOperations;
@Autowired
private ReactiveCassandraOperations reactiveCassandraOperations;
与所有 Spring 自动装配一样,这假定 ApplicationContext 中只有一个 [Reactive]CassandraOperations 类型的 bean。如果您有多个 [Reactive]CassandraTemplate bean(当您在同一项目中使用多个键空间时就是这种情况),那么您可以使用 @Qualifier 注解来指定想要自动装配的 bean。
- Imperative
- Reactive
@Autowired
@Qualifier("keyspaceOneTemplateBeanId")
private CassandraOperations cassandraOperations;
@Autowired
@Qualifier("keyspaceOneTemplateBeanId")
private ReactiveCassandraOperations reactiveCassandraOperations;
通过 ApplicationContext 查找 Bean
您也可以从 ApplicationContext 中查找 [Reactive]CassandraTemplate bean,如下例所示:
- Imperative
- Reactive
CassandraOperations cassandraOperations = applicationContext.getBean("cassandraTemplate", CassandraOperations.class);
ReactiveCassandraOperations cassandraOperations = applicationContext.getBean("ReactiveCassandraOperations", ReactiveCassandraOperations.class);
查询行
您可以使用 Query 和 Criteria 类来表达查询,这两个类的方法名反映了 Cassandra 原生命谓词运算符的名称,例如 lt、lte、is 等。
Query 和 Criteria 类采用流式 API 风格,以便您可以轻松地将多个方法条件和查询串联在一起,同时使代码易于理解。在 Java 中创建 Query 和 Criteria 实例时会使用静态导入,以提高可读性。
查询表中的行
在前面的章节中,我们了解了如何使用 [Reactive]CassandraTemplate 上的 selectOneById 方法来检索单个对象。这样做会返回单个领域对象。我们也可以查询一组行,并将其作为领域对象列表返回。假设我们有若干 Person 对象,其姓名和年龄值作为行存储在表中,且每个人都有一个账户余额,那么我们现在可以使用以下代码运行查询:
- Imperative
- Reactive
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);
import static org.springframework.data.cassandra.core.query.Criteria.where;
import static org.springframework.data.cassandra.core.query.Query.query;
…
Flux<Person> result = reactiveCassandraTemplate.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 类的方法
-
CriteriaDefinitiongt(Object value):使用>运算符创建一个条件。 -
CriteriaDefinitiongte(Object value):使用>=运算符创建一个条件。 -
CriteriaDefinitionin(Object… values):对可变参数使用IN运算符创建一个条件。 -
CriteriaDefinitionin(Collection<?> collection):使用集合、通过IN运算符创建一个条件。 -
CriteriaDefinitionis(Object value):使用字段匹配(column = value)创建一个条件。 -
CriteriaDefinitionlt(Object value):使用<运算符创建一个条件。 -
CriteriaDefinitionlte(Object value):使用⇐运算符创建一个条件。 -
CriteriaDefinitionlike(Object value):使用LIKE运算符创建一个条件。 -
CriteriaDefinitioncontains(Object value):使用CONTAINS运算符创建一个条件。 -
CriteriaDefinitioncontainsKey(Object key):使用CONTAINS KEY运算符创建一个条件。
Criteria 一旦创建即不可变。
Query 类的方法
Query 类还有一些附加方法,您可以使用它们为查询提供选项:
-
Queryby(CriteriaDefinition… criteria):用于创建Query对象。 -
Queryand(CriteriaDefinition criteria):用于向查询添加附加条件。 -
Querycolumns(Columns columns):用于定义要包含在查询结果中的列。 -
Querylimit(Limit limit):用于将返回结果的大小限制为所提供的限制(用于SELECT限制)。 -
Querylimit(long limit):用于将返回结果的大小限制为所提供的限制(用于SELECT限制)。 -
QuerypageRequest(Pageable pageRequest):用于将Sort、PagingState和fetchSize与查询关联起来(用于分页)。 -
QuerypagingState(CassandraScrollPosition pagingState):用于将CassandraScrollPosition与查询关联起来(用于分页)。 -
QuerypagingState(ByteBuffer pagingState):用于将原始ByteBuffer与查询关联起来(用于分页)。 -
QueryqueryOptions(QueryOptions queryOptions):用于将QueryOptions与查询关联起来。 -
Querysort(Sort sort):用于为结果提供排序定义。 -
QuerywithAllowFiltering():用于生成ALLOW FILTERING查询。
Query 一旦创建即不可变。调用方法会创建新的不可变(中间)Query 对象。
查询行的方法
Query 类具有以下返回行的方法:
-
List<T>select(Query query, Class<T> entityClass):从表中查询一个类型为T的对象列表。 -
TselectOne(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的对象列表。 -
TselectOne(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:
- Imperative
- Reactive
List<SWCharacter> all = ops.query(SWCharacter.class)
.inTable("star_wars") 1
.all();
Skip this step if
SWCharacterdefines the table name with@Tableor if using the class name as the table name is not a problem
Flux<SWCharacter> all = ops.query(SWCharacter.class)
.inTable("star_wars") 1
.all();
Skip this step if
SWCharacterdefines the table name with@Tableor if using the class name as the table name is not a problem
如果 Cassandra 中的一个表保存了不同类型的实体,例如在一个 SWCharacters 表中保存了一个 Jedi,那么您可以使用不同的类型来映射查询结果。您可以使用 as(Class<?> targetType) 将结果映射到不同的目标类型,而 query(Class<?> entityType) 仍然应用于查询和表名。以下示例使用了 query 和 as 方法:
- Imperative
- Reactive
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
SWCharactertype.Resulting rows are mapped into
Jedi.
Flux<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
SWCharactertype.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 的手段。
以下示例使用保存操作并检索其内容:
- Imperative
- Reactive
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);
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);
Mono<Person> queriedBob = reactiveCassandraTemplate.selectOneById(query(where("age").is(33)), Person.class);
您可以使用以下操作来插入和保存:
-
voidinsert(Object objectToSave):将对象插入 Apache Cassandra 表中。 -
WriteResultinsert(Object objectToSave, InsertOptions options):将对象插入 Apache Cassandra 表中并应用InsertOptions。
您可以使用以下更新操作:
-
voidupdate(Object objectToSave):更新 Apache Cassandra 表中的对象。 -
WriteResultupdate(Object objectToSave, UpdateOptions options):更新 Apache Cassandra 表中的对象并应用UpdateOptions。
您也可以使用传统方式,自己编写 CQL 语句,如下例所示:
- Imperative
- Reactive
String cql = "INSERT INTO person (age, name) VALUES (39, 'Bob')";
cassandraTemplate().getCqlOperations().execute(cql);
String cql = "INSERT INTO person (age, name) VALUES (39, 'Bob')";
Mono<Boolean> applied = reactiveCassandraTemplate.getReactiveCqlOperations().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 奖金,来更新单个账户对象:
- Imperative
- Reactive
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);
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;
…
Mono<Boolean> wasApplied = reactiveCassandraTemplate.update(Query.query(where("id").is("foo")),
Update.create().increment("balance", 50.00), Account.class);
除了前面讨论的 Query 之外,我们还通过使用 Update 对象来提供更新定义。Update 类具有与 Apache Cassandra 可用的更新赋值相匹配的方法。
大多数方法会返回 Update 对象,以便为代码风格提供流式 API。
执行行更新的方法
更新方法可以更新行,如下所示:
booleanupdate(Query query, Update update, Class<?> entityClass):更新 Apache Cassandra 表中所选的一组对象。
Update 类的方法
Update 类可以配合一点“语法糖”来使用,因为其方法本就意在串联在一起。此外,您还可以通过静态方法 public static Update update(String key, Object value) 并使用静态导入,来快速创建一个新的 Update 实例。
Update 类具有以下方法:
-
AddToBuilderaddTo(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 中。
-
-
Updateremove(String columnName, Object value):通过使用-更新赋值从集合中移除该值。 -
Updateclear(String columnName):清空集合。 -
Updateincrement(String columnName, Number delta):通过使用+更新赋值进行更新。 -
Updatedecrement(String columnName, Number delta):通过使用-更新赋值进行更新。 -
Updateset(String columnName, Object value):通过使用=更新赋值进行更新。 -
SetBuilderset(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 对象。
删除行的方法
您可以使用以下重载方法从数据库中移除对象:
-
booleandelete(Query query, Class<?> entityClass):删除由Query选中的对象。 -
Tdelete(T entity):删除给定对象。 -
Tdelete(T entity, QueryOptions queryOptions):应用QueryOptions删除给定对象。 -
booleandeleteById(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而失败。
乐观锁定仅支持单实体操作,不支持批量操作。