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作为UUIDproductNames作为数组类型来获取匹配的产品。

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未找到对应于cqlTypejavaType的类型:

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上找到。