1. 概述
在这个教程中,我们将学习如何使用 Spring Data Cassandra 实现一个查询,以获取多条记录。
我们将使用IN
子句来指定列的多个值。我们还将看到在测试时遇到的一个意外错误。最后,我们将理解根本原因并修复问题。
2. 在 Spring Data Cassandra 中使用 IN
运算符实现查询
让我们设想构建一个简单的应用程序,该程序查询Cassandra数据库以获取一条或多条记录。
我们可以在WHERE
子句中使用IN
运算符,为列指定多个可能的值。
2.1. IN
运算符的使用理解
在构建应用之前,让我们理解这个运算符的用法。
IN
条件只允许应用于分区键的最后一个列,如果我们要对所有前导键列进行等值查询。同样,我们也可以遵循相同规则在任何分片键列中使用它。
让我们通过product
表的例子来看:
CREATE TABLE mykeyspace.product (
product_id uuid,
product_name text,
description text,
price float,
PRIMARY KEY (product_id, product_name)
)
假设我们要找到具有相同一组product_id
的产品:
cqlsh:mykeyspace> select * from product where product_id in (2c11bbcd-4587-4d15-bb57-4b23a546bd7e, 2c11bbcd-4587-4d15-bb57-4b23a546bd22);
product_id | product_name | description | price
--------------------------------------+--------------+-----------------+-------
2c11bbcd-4587-4d15-bb57-4b23a546bd22 | banana | banana | 6.05
2c11bbcd-4587-4d15-bb57-4b23a546bd22 | banana v2 | banana v2 | 8.05
2c11bbcd-4587-4d15-bb57-4b23a546bd22 | banana v3 | banana v3 | 6.25
2c11bbcd-4587-4d15-bb57-4b23a546bd7e | banana chips | banana chips | 10.05
在上述查询中,我们在product_id
列上应用了IN
子句,且没有其他前导主键需要包含。
类似地,我们可以找到所有具有相同产品名称的产品:
cqlsh:mykeyspace> select * from product where product_id = 2c11bbcd-4587-4d15-bb57-4b23a546bd22 and product_name in ('banana', 'banana v2');
product_id | product_name | description | price
--------------------------------------+--------------+-----------------+-------
2c11bbcd-4587-4d15-bb57-4b23a546bd22 | banana | banana | 6.05
2c11bbcd-4587-4d15-bb57-4b23a546bd22 | banana v2 | banana v2 | 8.05
在上述查询中,我们在所有前导键(即product_id
)上应用了等值检查。
请注意,WHERE
子句应包含与主键定义中相同的列顺序。
接下来,我们将在这个Spring数据应用中实现这个查询。
2.2. Maven依赖项
我们将添加[spring-boot-starter-data-cassandra](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-cassandra)
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-cassandra</artifactId>
<version>3.1.5</version>
</dependency>
2.3. 实现 Spring Data仓库
我们将通过扩展CassandraRepository
接口来实现查询。
首先,我们来实现上面的product
表,带有几个属性:
@Table
public class Product {
@PrimaryKeyColumn(name = "product_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
private UUID productId;
@PrimaryKeyColumn(name = "product_name", ordinal = 1, type = PrimaryKeyType.CLUSTERED)
private String productName;
@Column("description")
private String description;
@Column("price")
private double price;
}
在Product
类中,我们标记productId
为分区键,productName
为分片键。这两个列一起构成了主键。
现在,假设我们试图找到一个productId
和多个productName
匹配的产品。
我们将实现ProductRepository
接口,使用IN
查询:
@Repository
public interface ProductRepository extends CassandraRepository<Product, UUID> {
@Query("select * from product where product_id = :productId and product_name in :productNames")
List<Product> findByProductIdAndNames(@Param("productId") UUID productId, @Param("productNames") String[] productNames);
}
在上述查询中,我们将productId
作为UUID
,productNames
作为数组类型来获取匹配的产品。
Cassandra不允许在不包含所有主键的情况下查询非主键列。这是因为在多个节点上执行此类查询的性能不可预测。
或者,我们可以使用IN
或其他条件对任何列使用ALLOW FILTERING
选项:
cqlsh:mykeyspace> select * from product where product_name in ('banana', 'apple') and price=6.05 ALLOW FILTERING;
ALLOW FILTERING
选项可能会对性能产生潜在影响,我们应该谨慎使用。
3. 实现对 ProductRepository
的测试
现在,让我们通过使用Cassandra容器实例来实现ProductRepository
的测试案例。
3.1. 设置测试容器
为了实验,我们需要一个测试容器来运行Cassandra。我们将使用testcontainers库来设置容器。
需要注意的是,testcontainers
库需要运行中的Docker环境才能正常工作。
让我们添加[testcontainers](https://mvnrepository.com/artifact/org.testcontainers/testcontainers)
和[testcontainers-cassandra](https://mvnrepository.com/artifact/org.testcontainers/cassandra)
依赖:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>cassandra</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
3.2. 启动测试容器
首先,我们在测试类上设置Testcontainers
注解:
@Testcontainers
@SpringBootTest
class ProductRepositoryIntegrationTest { }
接下来,我们定义Cassandra容器对象,并在指定端口上暴露它:
@Container
private static final CassandraContainer cassandra = new CassandraContainer("cassandra:3.11.2")
.withExposedPorts(9042);
最后,让我们配置一些连接相关的属性,并创建Keyspace
:
@BeforeAll
static void setupCassandraConnectionProperties() {
System.setProperty("spring.cassandra.keyspace-name", "mykeyspace");
System.setProperty("spring.cassandra.contact-points", cassandra.getHost());
System.setProperty("spring.cassandra.port", String.valueOf(cassandra.getMappedPort(9042)));
createKeyspace(cassandra.getCluster());
}
static void createKeyspace(Cluster cluster) {
try (Session session = cluster.connect()) {
session.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE_NAME + " WITH replication = \n" +
"{'class':'SimpleStrategy','replication_factor':'1'};");
}
}
3.3. 实现集成测试
为了测试,我们将使用上述ProductRepository
查询获取一些现有产品。
现在,让我们完成测试并验证检索功能:
UUID productId1 = UUIDs.timeBased();
Product product1 = new Product(productId1, "Apple", "Apple v1", 12.5);
Product product2 = new Product(productId1, "Apple v2", "Apple v2", 15.5);
UUID productId2 = UUIDs.timeBased();
Product product3 = new Product(productId2, "Banana", "Banana v1", 5.5);
Product product4 = new Product(productId2, "Banana v2", "Banana v2", 15.5);
productRepository.saveAll(List.of(product1, product2, product3, product4));
List<Product> existingProducts = productRepository.findByProductIdAndNames(productId1, new String[] {"Apple", "Apple v2"});
assertEquals(2, existingProducts.size());
assertTrue(existingProducts.contains(product1));
assertTrue(existingProducts.contains(product2));
预期上述测试应该通过。然而,我们将从ProductRepository
那里得到一个意外的错误:
com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException: Codec not found for requested operation: [List(TEXT, not frozen]
<-> [Ljava.lang.String;]
at com.datastax.oss.driver.internal.core.type.codec.registry.CachingCodecRegistry.createCodec(CachingCodecRegistry.java:609)
at com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry$1.load(DefaultCodecRegistry.java:95)
at com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry$1.load(DefaultCodecRegistry.java:92)
at com.datastax.oss.driver.shaded.guava.common.cache.LocalCache$LoadingValueReference.loadFuture(LocalCache.java:3527)
....
at com.datastax.oss.driver.internal.core.data.ValuesHelper.encodePreparedValues(ValuesHelper.java:112)
at com.datastax.oss.driver.internal.core.cql.DefaultPreparedStatement.boundStatementBuilder(DefaultPreparedStatement.java:187)
at org.springframework.data.cassandra.core.PreparedStatementDelegate.bind(PreparedStatementDelegate.java:59)
at org.springframework.data.cassandra.core.CassandraTemplate$PreparedStatementHandler.bindValues(CassandraTemplate.java:1117)
at org.springframework.data.cassandra.core.cql.CqlTemplate.query(CqlTemplate.java:541)
at org.springframework.data.cassandra.core.cql.CqlTemplate.query(CqlTemplate.java:571)...
at com.sun.proxy.$Proxy90.findByProductIdAndNames(Unknown Source)
at org.baeldung.inquery.ProductRepositoryIntegrationTest$ProductRepositoryLiveTest.givenExistingProducts_whenFindByProductIdAndNames_thenProductsIsFetched(ProductRepositoryNestedLiveTest.java:113)
接下来,让我们详细调查错误。
3.4. 错误的根本原因
日志表明测试未能成功获取产品,出现了一个内部CodecNotFoundException
异常。CodecNotFoundException
异常表示请求操作所针对的查询参数类型未找到。
异常类显示codec未找到对应于cqlType
和javaType
的类型:
public CodecNotFoundException(@Nullable DataType cqlType, @Nullable GenericType<?> javaType) {
this(String.format("Codec not found for requested operation: [%s <-> %s]", cqlType, javaType), (Throwable)null, cqlType, javaType);
}
CQL数据类型包括所有常见的基本、集合和用户定义类型,但数组是不允许的。在某些早期版本的Spring Data Cassandra,如1.3.x
,列表类型也不被支持。
4. 修复查询
要解决错误,我们需要在ProductRepository
接口中添加一个有效的查询参数类型。
我们将把请求参数类型从数组更改为List
:
@Query("select * from product where product_id = :productId and product_name in :productNames")
List<Product> findByProductIdAndNames(@Param("productId") UUID productId, @Param("productNames") List<String> productNames);
最后,我们重新运行测试并验证查询是否有效:
givenExistingProducts_whenFindByIdAndNamesIsCalled_thenProductIsReturned: 1 total, 1 passed
5. 总结
在这篇文章中,我们学习了如何在Cassandra中使用Spring Data Cassandra实现IN
查询子句。我们在测试时遇到了一个意外的错误,并理解了其根本原因。我们还看到了如何通过在方法参数中使用有效的Collection
类型来修复问题。
如往常一样,示例代码可以在GitHub上找到。