Cassandra – 使用 DataStax Java 驅動程序進行對象映射
一、概述
在本教程中,我們將學習如何使用DataStax Java 驅動程序將對象映射到 Cassandra 表。
我們將了解如何使用 Java 驅動程序定義實體、創建 DAO 以及對 Cassandra 表執行 CRUD 操作。
2.項目設置
我們將使用 Spring Boot 框架創建一個與 Cassandra 數據庫交互的簡單應用程序。我們將使用 Java 驅動程序創建表、實體和 DAO。然後,我們將使用 DAO 對錶執行 CRUD 操作。
2.1.依賴關係
讓我們首先將所需的依賴項添加到我們的項目中。我們將使用Cassandra 的 Spring Boot starter連接到數據庫:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-cassandra</artifactId> </dependency>
此外,我們將添加java-driver-mapper-runtime
依賴項以將對象映射到 Cassandra 表:
<dependency> <groupId>com.datastax.oss</groupId> <artifactId>java-driver-mapper-runtime</artifactId> <version>4.13.0</version> </dependency>
最後,讓我們配置註釋處理器以在編譯時生成 DAO 和映射器:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <annotationProcessorPaths> <path> <groupId>com.datastax.oss</groupId> <artifactId>java-driver-mapper-processor</artifactId> <version>4.13.0</version> </path> </annotationProcessorPaths> </configuration> </plugin>
3. Cassandra實體
讓我們定義一個可用於映射到 Cassandra 表的實體。我們將創建一個代表user_profile
表的User
類:
`@Entity
public class User {
@PartitionKey
private int id;
private String userName;
private int userAge;
// getters and setters
}`
@Entity
註釋告訴映射器將此類映射到表。 @PartitionKey
註釋告訴映射器使用id
字段作為表的分區鍵。
映射器使用默認構造函數來創建實體的新實例。因此,我們需要確保可以訪問默認的無參數構造函數。如果我們創建一個非默認構造函數,我們需要顯式聲明默認構造函數。
默認情況下,實體是可變的,因此我們必須聲明 getter 和 setter。我們將在本教程後面看到如何更改此行為。
3.1.命名策略
@NamingStrategy
註釋允許我們為表和列指定命名約定。默認的命名策略是NamingConvention.SNAKE_CASE_INSENSITIVE.
它在與數據庫交互時將類名和字段名轉換為蛇形命名法。
例如,默認情況下, userName
字段映射到數據庫中的user_name
列。如果我們將命名策略更改為NamingConvention.LOWER_CAMEL_CASE
,則userName
字段將映射到數據庫中的userName
列。
3.2.物業策略
@PropertyStrategy
註釋指定映射器將如何訪問實體的屬性。它具有三個屬性mutable
、 getterStyle
和setterStyle
。
mutable
屬性告訴映射器實體是否可變。默認情況下是true
。如果我們將其設置為false
,映射器將使用“all columns”
構造函數來創建實體的新實例。
“all columns”
構造函數是一個將表的所有列作為參數的構造函數,其順序與它們在實體中定義的順序相同。例如,如果我們有一個包含以下字段的實體: id
、 userName
和userAge
, “all columns”
構造函數將如下所示:
public User(int id, String userName, int userAge) { this.id = id; this.userName = userName; this.userAge = userAge; }
除此之外,實體應該有 getter 但不需要有 setter。可選地,按照慣例,字段可以是final
。
getterStyle
和setterStyle
屬性告訴映射器如何找到實體的 getter 和 setter。它們都有兩個可能的值——FLUENT 和 JAVA_BEANS。
如果設置為 FLUENT,映射器將查找與字段同名的方法。例如,如果該字段是userName
,則映射器將查找名為userName()
的方法。
如果設置為 JAVA_BEANS,映射器將查找帶有get
或set
前綴的方法。例如,如果該字段是userName
,則映射器將查找名為getUserName()
的方法。
對於普通的 Java 類, getterStyle
和setterStyle
屬性默認設置為JAVA_BEANS
。但是,對於 Records,它們默認設置為 FLUENT。同樣,Records 的mutable
屬性默認設置為false
。
3.3. @CqlName
@CqlName
註解指定 Cassandra 數據庫中表或列的名稱。由於我們要將User
實體映射到user_profile
表,並將userName
字段映射到表中的username
名列,我們可以使用@CqlName
註解:
`@Entity
@CqlName("user_profile")
public class User {
@PartitionKey
private int id;
@CqlName("username")
private String userName;
private int userAge;
// getters and setters
}`
遵循默認或指定命名策略的字段不需要註釋。
3.4. @PartitionKey
和@ClusteringColumn
分區鍵和集群列分別使用@PartitionKey
和@ClusteringColumn
註釋定義。在我們的例子中, id
字段是分區鍵。如果我們想按userAge
字段對行進行排序,我們可以將@ClusteringColumn
註釋添加到userAge
字段。
@ClusteringColumn private int userAge;
可以在實體中定義多個分區鍵和集群列。可以通過在註釋中傳遞順序來指定分區的順序。例如,如果我們想按id
然後按userName
對錶進行分區,我們可以執行以下操作:
@PartitionKey(1) private int id; @PartitionKey(2) @CqlName("username")
並且類似地用於聚類列。
3.5. @Transient
@Transient
註釋告訴映射器忽略該字段。標記為@Transient
字段不會映射到數據庫中的列。它只會是 Java 對象的一部分。映射器不會嘗試從數據庫讀取或寫入字段的值。
除了在字段上使用@Transient
註解,我們還可以在實體上使用@TransientProperties
註解,將多個字段標記為transient。
3.6. @Computed
標記為@Computed
字段映射到數據庫中的列,但它們不能由客戶端設置。它們由存儲在服務器上的數據庫函數計算得出。
假設我們要向存儲行的寫入時間戳的實體添加一個新字段。我們可以像下面這樣添加一個實體:
@Computed("writetime(userName)") private long writeTime;
創建User
記錄時,映射器將調用writetime()
方法並將字段writeTime
的值設置為函數的結果。
4.層次實體
實體可以使用繼承來定義。這可能是對具有大量公共字段的實體建模的好方法。
例如,我們可以有一個user_profile
表,其中包含所有用戶的公共字段。然後我們可以有一個admin_profile
表,其中包含用於管理員的附加字段。
在這種情況下,我們可以為user_profile
表定義一個實體,然後擴展它為admin_profile
表創建一個實體:
`@Entity
@CqlName("admin_profile")
public class Admin extends User {
private String role;
private String department;
// getters and setters
}`
Admin
實體將具有User
實體的所有字段以及role
和department
的附加字段。我們應該注意, @Entity
註釋僅在具體類上是必需的。抽像類或接口不需要它。
4.1.層次實體中的不變性
如果子類不可變,則子類的“all columns”
構造函數需要調用父類的“all columns”
構造函數。在這種情況下,參數順序應該是先傳遞子類的參數,再傳遞父類的參數。
例如,我們可以為 Admin 實體創建一個“all columns”
構造函數:
public Admin(String role, String department, int id, String userName, int userAge) { super(id, userName, userAge); this.role = role; this.department = department; }
4.2. @HierarchyScanStrategy
@HierarchyScanStrategy
註釋指定映射器應如何掃描實體的層次結構以獲取註釋。
它具有三個字段:
-
scanAncestors
默認情況下,它設置為true
,映射器將掃描實體的整個層次結構。當設置為false
時,映射器將只掃描實體。 -
highestAncestor
當設置為一個類時,映射器將掃描實體的層次結構,直到它到達指定的類。指定類以上的類將不會被掃描。 -
includeHighestAncestor
– 當設置為true
時,映射器還將掃描指定的 highestAncestor。默認情況下,映射器只會掃描指定類之上的類。
讓我們看看如何使用這些註釋:
`@Entity
@HierarchyScanStrategy(highestAncestor = User.class, includeHighestAncestor = true)
public class Admin extends User {
private String role;
private String department;
// getters and setters
}`
通過將highestAncestor
屬性設置為User.class
,映射器將掃描Admin
實體的層次結構,直到它到達User
實體。
我們已將includeHighestAncestor
設置為true
,因此映射器還將掃描User
實體。默認情況下,該屬性設置為false
,因此映射器不會掃描User
實體。
掃描器不會掃描User
實體擴展的任何實體。
5.DAO接口
映射器提供了一個在 Cassandra 數據庫上執行操作的 DAO 接口。我們可以使用@Dao
註解來創建一個DAO 接口。接口的方法必須具有映射器提供的註解之一。
5.1. CRUD 註釋
映射器提供以下註釋來對數據庫執行基本的 CRUD 操作:
-
@Insert
– 在數據庫中插入一行 -
@Select
– 創建帶有指定參數的選擇查詢並返回結果。結果可以是單個實體或實體列表。 -
@Update
– 更新數據庫中的一行 -
@Delete
– 從數據庫中刪除一行
讓我們看看如何使用這些註釋:
`@Dao
public interface UserDao {
@Insert
void insertUser(User user);
@Select
User getUserById(int id);
@Select
PagingIterable
@Update
void updateUser(User user);
@Delete
void deleteUser(User user);
}`
需要注意的重點是方法的參數應該與註釋的允許參數相匹配。
插入、更新和刪除方法應該有一個參數,即要插入、更新或刪除的實體。
select 方法有兩個選項:
- 實體的完整主鍵——參數以分區鍵開頭,然後是按它們在實體中應用的順序排列的聚類列。在這種情況下,該方法將返回單個實體。
- 主鍵的子集——在這種情況下,該方法將返回一個實體列表。
5.2.使用@Query
的自定義查詢
有兩種方法可以對數據庫執行自定義查詢。我們可以使用@Query
或@QueryProvider
註釋。
我們先看@Query
註解:
`@Dao
public interface UserDao {
@Query("select * from user_profile where user_age > :userAge ALLOW FILTERING")
PagingIterable
}`
ALLOW FILTERING
子句是必需的,因為我們在未指定分區鍵的情況下對二級索引執行查詢。此類查詢可能需要更長的時間,應避免在大型數據集上進行。
當是簡單的查詢時,我們可以使用@Query
註解。當查詢比較複雜時,可能需要使用核心驅動來執行查詢。我們可以使用@QueryProvider
註解來做到這一點。
5.3.使用@QueryProvider
自定義查詢
@QueryProvider
註釋採用一個負責查詢執行並返回結果的類。
讓我們為上述查詢創建一個查詢提供程序:
public class UserQueryProvider {
private final CqlSession session;
private final EntityHelper<User> userHelper;
public UserQueryProvider(MapperContext context, EntityHelper<User> userHelper) {
this.session = context.getSession();
this.userHelper = userHelper;
}
public PagingIterable<User> getUsersOlderThanAge(String age) {
SimpleStatement statement = QueryBuilder.selectFrom("user_profile")
.all()
.whereColumn("user_age")
.isGreaterThan(QueryBuilder
.bindMarker(age))
.build();
PreparedStatement preparedSelectUser = session.prepare(statement);
return session
.execute(preparedSelectUser.getQuery())
.map(result -> userHelper.get(result, true));
}
}
實體助手用於將查詢結果轉換為實體。映射器自動為實體創建實體幫助器 bean,因此該 bean 將存在用於自動裝配。
現在,我們可以使用@QueryProvider
註釋來使用查詢提供程序:
`@Dao
public interface UserDao {
@QueryProvider(providerClass = UserQueryProvider.class, entityHelpers = User.class, providerMethod = "getUsersOlderThanAge")
PagingIterable
}`
providerClass
字段指定查詢提供程序類, entityHelpers
字段指定查詢中使用的實體類。 providerMethod
字段指定執行查詢的查詢提供程序類中的方法。
如果查詢不使用任何實體,則不需要指定entityHelpers
字段。如果方法名稱與 DAO 接口中的方法名稱相同,則也不必指定providerMethod
字段。
5.4. @GetEntity
和@SetEntity
有時,可能需要在 Cassandra 的核心驅動程序和映射器的操作之間切換。如果出現這樣的需求,我們可以使用@GetEntity
和@SetEntity
註解來定義在兩者之間轉換的方法。
讓我們看看如何使用這些註釋:
`@GetEntity
User getUser(Row row);
@SetEntity
UdtValue setUser(UdtValue udtValue, User user);`
@GetEntity
註釋告訴映射器該方法將Row
轉換為實體。當我們想要使用核心驅動程序執行查詢然後將結果轉換為實體時,這會有所幫助。
@SetEntity
註釋告訴映射器該方法將實體轉換為SettableByName
對象。第一個參數是將被更新和返回的對象。第二個參數是將提供要設置的值的實體。
如果SettableByName
對像是BoundStatement
之類的語句,映射器會自動將參數綁定到語句並返回語句。這在使用核心驅動程序但使用實體進行其他操作的語句時很有用。
當使用像UdtValue
這樣的值對象時,該方法將User
對象轉換為通用UdtValue
對象。這在使用實體進行數據庫交互但使用核心驅動程序庫進行結果集時很有用。
5.5.櫃檯桌
Cassandra 中的計數器存儲在單獨的表中。映射器提供了一種增加計數器表中計數器值的方法。首先,讓我們為計數器表創建一個實體:
`@Entity
public class Counter {
@PartitionKey
private String id;
private long count;
// getters, setters and constructors
}`
我們應該注意,一個計數器表應該只有一個計數器列和分區鍵。計數器列應為long
類型。表中不能有其他數據列。
5.6.遞增計數器
現在,我們可以為計數器表創建一個 DAO:
`@Dao
public interface CounterDao {
@Increment(entityClass = Counter.class)
void incrementCounter(String id, long count);
@Select
Counter getCounterById(String id);
}`
我們先看看@Increment
方法。它用於創建和更新計數器。
首先,需要為實體類提供entityClass
屬性。接下來,該方法將所有分區鍵列和集群鍵列作為參數。最後,最後一個參數將是我們要增加字段的值。
要找到要遞增的列,我們可以用@CqlName
註釋最後一個參數並指定確切的列名。如果參數沒有註釋,映射器會查找與參數同名的字段。在這種情況下,參數名稱是count
並且映射器在實體類中查找名稱為count
字段。
計數器表的 DAO 只能有@Increment
、 @Select,
和@Delete
方法。
映射器不允許我們使用@Update
方法更新整個計數器行。我們也不能使用@Insert
方法向計數器表中插入新行。如果我們嘗試這樣做,映射器將拋出異常。如果不存在, @Increment
方法本身將創建一個新行。
6.映射器接口
映射器接口是映射器的入口點。它提供了獲取 DAO 實例的方法。我們可以使用@Mapper
註解來創建映射器接口。對於返回 DAO 實例的方法,我們可以使用@DaoFactory
註釋。
讓我們創建一個映射器接口:
`@Mapper
public interface DaoMapper {
@DaoFactory
UserDao getUserDao(@DaoKeyspace CqlIdentifier keyspace);
@DaoFactory
CounterDao getUserCounterDao(@DaoKeyspace CqlIdentifier keyspace);
}`
@DaoFactory
註釋創建一個 DAO 實例。 @DaoKeyspace
註釋指定用於 DAO 實例的鍵空間。該接口還負責 DAO 實例的生命週期。 DAO 實例與 Cassandra 會話具有相同的生命週期。
7. 測試
讓我們看看如何測試映射器。我們將創建一個測試類,它將使用映射器提供的 DAO 對數據庫執行操作。
讓我們從創建一個測試類開始,在 Cassandra 中創建表,並設置 DAO。
要運行測試,應運行 Cassandra 數據庫實例並配置連接。或者,我們可以使用 testcontainers 來設置一個臨時實例。
7.1 創建表和 DAO
在使用 DAO 之前,我們需要創建表。我們可以通過直接在 Cassandra 數據庫上運行查詢來創建表,也可以使用CQLSession
以編程方式創建表。
讓我們通過在setup()
方法中執行 CQL 語句來創建表:
class MapperLiveTest {
static UserDao userDao;
static CounterDao counterDao;
@BeforeAll
static void setup() {
CqlSession session = CqlSession.builder().build();
String createKeyspace = "CREATE KEYSPACE IF NOT EXISTS baeldung " +
"WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};";
String useKeyspace = "USE baeldung;";
String createUserTable = "CREATE TABLE IF NOT EXISTS user_profile " +
"(id int, username text, user_age int, writetime bigint, PRIMARY KEY (id, user_age)) " +
"WITH CLUSTERING ORDER BY (user_age DESC);";
String createAdminTable = "CREATE TABLE IF NOT EXISTS admin_profile " +
"(id int, username text, user_age int, role text, writetime bigint, department text, " +
"PRIMARY KEY (id, user_age)) " +
"WITH CLUSTERING ORDER BY (user_age DESC);";
String createCounter = "CREATE TABLE IF NOT EXISTS counter " +
"(id text, count counter, PRIMARY KEY (id));";
session.execute(createKeyspace);
session.execute(useKeyspace);
session.execute(createUserTable);
session.execute(createAdminTable);
session.execute(createCounter);
DaoMapper mapper = new DaoMapperBuilder(session).build();
userDao = mapper.getUserDao(CqlIdentifier.fromCql("baeldung"));
counterDao = mapper.getUserCounterDao(CqlIdentifier.fromCql("baeldung"));
}
// ...
}
我們已經創建了查詢來創建鍵空間、表和計數器表。
我們還創建了一個DaoMapper
實例並從中獲取了 DAO 實例。
註解處理器自動生成DaoMapperBuilder
類。構建器將CqlSession
實例作為參數並返回DaoMapper
實例。上面定義的DaoMapper
方法提供了 DAO 實例。
7.2.測試用戶 DAO
讓我們編寫一些測試來查看調用 DAO 方法的語法。我們將從測試UserDao
實例開始。
讓我們創建一個用戶並從數據庫中檢索它:
@Test void givenUser_whenInsert_thenRetrievedDuringGet() { User user = new User(1, "JohnDoe", 31); userDao.insertUser(user); User retrievedUser = userDao.getUserById(1); Assertions.assertEquals(retrievedUser.getUserName(), user.getUserName()); }
我們創建了一個User
對象,並使用insertUser()
方法將其插入到數據庫中。然後我們使用getUserById()
方法從數據庫中檢索用戶並驗證用戶名是否相同。
讓我們測試查詢提供程序方法:
@Test void givenUser_whenGetUsersOlderThan_thenRetrieved() { User user = new User(2, "JaneDoe", 20); userDao.insertUser(user); List<User> retrievedUsers = userDao.getUsersOlderThanAge(30).all(); Assertions.assertEquals(retrievedUsers.size(), 1); }
我們向數據庫添加了一個新用戶,然後使用getUsersOlderThanAge()
方法檢索了所有 30 歲以上的用戶。
getUsersOlderThanAge()
方法返回一個PagingIterable
實例。我們可以使用all()
方法來檢索所有結果。
該查詢將只返回一個用戶。
7.3.測試計數器 DAO
最後,讓我們看看如何使用計數器。讓我們創建一個遞增計數器的測試:
`@Test
void givenCounter_whenIncrement_thenIncremented() {
Counter users = counterDao.getCounterById("users");
long initialCount = users != null ? users.getCount(): 0;
counterDao.incrementCounter("users", 1);
users = counterDao.getCounterById("users");
long finalCount = users != null ? users.getCount(): 0;
Assertions.assertEquals(finalCount - initialCount, 1);
}`
我們首先獲取計數器的初始計數,然後將計數器加 1 並從數據庫中獲取最終計數。然後我們驗證最終計數是否比初始計數多 1。
八、結論
在本文中,我們了解了 DataStax Java Driver Mapper。我們已經了解瞭如何使用映射器對錶和計數器執行 CRUD 操作。
我們還看到了在預定義的 DAO 方法不夠用時如何使用映射器來使用查詢提供程序。
本文中使用的代碼示例可以在 GitHub 上找到。