Spring Data Cassandra 中使用 IN 子句進行查詢
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>2.7.11</version>
</dependency>
2.3.實施 Spring 資料儲存庫
讓我們透過擴展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
相符的所有產品。
我們將使用IN查詢實作ProductRepository
介面:
@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 不允許查詢非主鍵列。這是由於跨多個節點執行此類查詢時的效能無法預測。
或者,我們可以使用ALLOW FILTERING
選項在任何欄位上使用IN
或任何其他條件:
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
和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 = (CassandraContainer) new CassandraContainer("cassandra:3.11.2")
.withExposedPorts(9042);
最後,讓我們配置一些與連線相關的屬性並建立Keyspace
:
@BeforeAll
static void setupCassandraConnectionProperties() {
System.setProperty("spring.data.cassandra.keyspace-name", "mykeyspace");
System.setProperty("spring.data.cassandra.contact-points", cassandra.getHost());
System.setProperty("spring.data.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
異常表示未找到所請求操作的查詢參數類型。
異常類別顯示未找到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
中,也不支援List
類型。
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. 結論
在本文中,我們學習如何使用 Spring Data Cassandra 在 Cassandra 中實作IN
查詢子句。我們在測試時也遇到了意外錯誤並了解了根本原因。我們了解如何在方法參數中使用有效的Collection
類型來解決問題。
與往常一樣,範例程式碼可以在 GitHub 上找到。