對映

豐富的物件對映支援由 MappingCassandraConverter 提供。MappingCassandraConverter 擁有豐富的元資料模型,提供了完整的功能集,用於將領域物件對映到 CQL 表。

對映元資料模型透過使用領域物件上的註解來填充。然而,基礎設施並不侷限於只使用註解作為元資料來源。透過遵循一套約定,MappingCassandraConverter 也允許您在不提供任何額外元資料的情況下將領域物件對映到表。

在本章中,我們將介紹 MappingCassandraConverter 的功能、如何使用約定將領域物件對映到表,以及如何透過基於註解的對映元資料覆蓋這些約定。

物件對映基礎

本節涵蓋了 Spring Data 物件對映、物件建立、欄位和屬性訪問、可變性和不可變性的基礎知識。請注意,本節僅適用於不使用底層資料儲存(如 JPA)的物件對映的 Spring Data 模組。另請務必查閱特定於儲存的部分,以獲取特定於儲存的物件對映資訊,例如索引、自定義列或欄位名稱等。

Spring Data 物件對映的核心職責是建立領域物件的例項,並將儲存原生的資料結構對映到這些物件上。這意味著我們需要兩個基本步驟

  1. 透過使用暴露的建構函式之一來建立例項。

  2. 填充例項以例項化所有暴露的屬性。

物件建立

Spring Data 會自動嘗試檢測用於例項化該型別物件的持久化實體的建構函式。解析演算法如下:

  1. 如果存在一個用 @PersistenceCreator 註解的單一靜態工廠方法,則使用它。

  2. 如果只有一個建構函式,則使用它。

  3. 如果有多個建構函式並且只有一個用 @PersistenceCreator 註解,則使用它。

  4. 如果型別是 Java Record,則使用其規範建構函式。

  5. 如果存在無參建構函式,則使用它。其他建構函式將被忽略。

值解析假定建構函式/工廠方法引數名稱與實體屬性名稱匹配,即解析將按照填充屬性的方式進行,包括對映中的所有自定義(不同的資料儲存列或欄位名稱等)。這也要求類檔案中提供引數名稱資訊,或者建構函式上存在 @ConstructorProperties 註解。

值解析可以透過使用 Spring Framework 的 @Value 註解和特定於儲存的 SpEL 表示式進行自定義。有關更多詳細資訊,請查閱特定於儲存的對映部分。

物件建立內部機制

為了避免反射的開銷,Spring Data 物件建立預設使用在執行時生成的工廠類,該類將直接呼叫領域類的建構函式。也就是說,對於此示例型別

class Person {
  Person(String firstname, String lastname) { … }
}

我們將在執行時建立一個在語義上等同於它的工廠類

class PersonObjectInstantiator implements ObjectInstantiator {

  Object newInstance(Object... args) {
    return new Person((String) args[0], (String) args[1]);
  }
}

這使效能比反射提高了大約 10%。為了使領域類符合這種最佳化條件,它需要遵循一組約束:

  • 它不能是私有類

  • 它不能是非靜態內部類

  • 它不能是 CGLib 代理類

  • Spring Data 使用的建構函式不能是私有的

如果滿足其中任何條件,Spring Data 將回退到透過反射例項化實體。

屬性填充

一旦實體例項被建立,Spring Data 將填充該類的所有剩餘持久化屬性。除非實體建構函式已填充(即透過其建構函式引數列表消費),否則將首先填充識別符號屬性,以允許解析迴圈物件引用。之後,將實體例項上設定所有未被建構函式填充的非瞬態屬性。為此,我們使用以下演算法:

  1. 如果屬性是不可變的,但暴露了 with… 方法(見下文),我們使用 with… 方法建立一個帶有新屬性值的新實體例項。

  2. 如果定義了屬性訪問(即透過 getter 和 setter 訪問),我們將呼叫 setter 方法。

  3. 如果屬性是可變的,我們直接設定欄位。

  4. 如果屬性是不可變的,我們使用持久化操作要使用的建構函式(參見物件建立)來建立例項的副本。

  5. 預設情況下,我們直接設定欄位值。

屬性填充內部機制

與我們的物件構造最佳化類似,我們也使用 Spring Data 執行時生成的訪問器類來與實體例項互動。

class Person {

  private final Long id;
  private String firstname;
  private @AccessType(Type.PROPERTY) String lastname;

  Person() {
    this.id = null;
  }

  Person(Long id, String firstname, String lastname) {
    // Field assignments
  }

  Person withId(Long id) {
    return new Person(id, this.firstname, this.lastame);
  }

  void setLastname(String lastname) {
    this.lastname = lastname;
  }
}
生成的 Property Accessor
class PersonPropertyAccessor implements PersistentPropertyAccessor {

  private static final MethodHandle firstname;              (2)

  private Person person;                                    (1)

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

    if ("firstname".equals(name)) {
      firstname.invoke(person, (String) value);             (2)
    } else if ("id".equals(name)) {
      this.person = person.withId((Long) value);            (3)
    } else if ("lastname".equals(name)) {
      this.person.setLastname((String) value);              (4)
    }
  }
}
1 PropertyAccessor 持有底層物件的可變例項。這是為了能夠修改原本不可變的屬性。
2 預設情況下,Spring Data 使用欄位訪問來讀取和寫入屬性值。根據 private 欄位的可見性規則,使用 MethodHandles 與欄位進行互動。
3 該類暴露了一個 withId(…) 方法,用於設定識別符號,例如當例項插入到資料儲存並生成識別符號時。呼叫 withId(…) 會建立一個新的 Person 物件。所有後續的修改都將發生在新的例項中,而不會觸碰之前的例項。
4 使用屬性訪問允許直接呼叫方法,而無需使用 MethodHandles

這使效能比反射提高了大約 25%。為了使領域類符合這種最佳化條件,它需要遵循一組約束:

  • 型別不能位於預設包或 java 包下。

  • 型別及其建構函式必須是 public

  • 內部類必須是 static 的。

  • 所使用的 Java 執行時必須允許在原始 ClassLoader 中宣告類。Java 9 及更高版本會施加某些限制。

預設情況下,Spring Data 嘗試使用生成的屬性訪問器,如果檢測到限制,則回退到基於反射的屬性訪問器。

讓我們看看下面的實體

示例實體
class Person {

  private final @Id Long id;                                                (1)
  private final String firstname, lastname;                                 (2)
  private final LocalDate birthday;
  private final int age;                                                    (3)

  private String comment;                                                   (4)
  private @AccessType(Type.PROPERTY) String remarks;                        (5)

  static Person of(String firstname, String lastname, LocalDate birthday) { (6)

    return new Person(null, firstname, lastname, birthday,
      Period.between(birthday, LocalDate.now()).getYears());
  }

  Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)

    this.id = id;
    this.firstname = firstname;
    this.lastname = lastname;
    this.birthday = birthday;
    this.age = age;
  }

  Person withId(Long id) {                                                  (1)
    return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
  }

  void setRemarks(String remarks) {                                         (5)
    this.remarks = remarks;
  }
}
1 識別符號屬性是 final 的,但在建構函式中設定為 null。該類暴露了一個 withId(…) 方法,用於設定識別符號,例如當例項插入到資料儲存並生成識別符號時。由於建立了一個新的 Person 物件,原始例項保持不變。對於其他由儲存管理但可能需要因持久化操作而更改的屬性,通常也採用相同的模式。wither 方法是可選的,因為持久化建構函式(參見 6)實際上是一個複製建構函式,設定屬性將被轉換為建立一個應用了新識別符號值的新例項。
2 firstnamelastname 屬性是普通的不可變屬性,可能透過 getter 暴露。
3 age 屬性是不可變的,但它是從 birthday 屬性派生出來的。按照所示設計,資料庫值將優先於預設值,因為 Spring Data 使用唯一宣告的建構函式。即使希望優先使用計算值,重要的是該建構函式也要將 age 作為引數(可能忽略它),否則屬性填充步驟將嘗試設定 age 欄位,並且由於其不可變且沒有 with… 方法而失敗。
4 comment 屬性是可變的,透過直接設定其欄位來填充。
5 remarks 屬性是可變的,透過呼叫 setter 方法來填充。
6 該類暴露了一個工廠方法和一個建構函式用於物件建立。這裡的核心思想是使用工廠方法而不是額外的建構函式,以避免透過 @PersistenceCreator 進行建構函式消歧。相反,屬性的預設值處理在工廠方法中進行。如果您希望 Spring Data 使用工廠方法進行物件例項化,請使用 @PersistenceCreator 對其進行註解。

一般建議

  • 儘量使用不可變物件 — 不可變物件很容易建立,因為例項化物件只需呼叫其建構函式。此外,這還可以避免您的領域物件充斥著允許客戶端程式碼操作物件狀態的 setter 方法。如果您需要這些方法,最好將它們設定為包保護(package protected),以便只能由有限數量的同包型別呼叫。僅透過建構函式例項化比透過屬性填充快達 30%。

  • 提供一個全引數建構函式 — 即使您不能或不想將實體建模為不可變值,提供一個接受實體所有屬性作為引數(包括可變屬性)的建構函式仍然有價值,因為這使得物件對映可以跳過屬性填充,從而獲得最佳效能。

  • 使用工廠方法而非過載建構函式以避免 @PersistenceCreator — 為了獲得最佳效能,需要一個全引數建構函式,因此我們通常希望暴露更多特定於應用程式用例的建構函式,這些建構函式會省略諸如自動生成的識別符號等內容。使用靜態工廠方法來暴露全引數建構函式的這些變體是一種成熟的模式。

  • 確保您遵循允許使用生成的例項化器和屬性訪問器類的約束 — 

  • 對於需要生成的識別符號,仍應結合使用 final 欄位和全引數持久化建構函式(首選)或 with… 方法 — 

  • 使用 Lombok 避免樣板程式碼 — 由於持久化操作通常需要一個接受所有引數的建構函式,它們的宣告會變成將引數賦給欄位的乏味重複樣板程式碼,最好使用 Lombok 的 @AllArgsConstructor 來避免。

覆蓋屬性

Java 允許靈活設計領域類,子類可以定義一個在其超類中已經宣告的同名屬性。考慮以下示例

public class SuperType {

   private CharSequence field;

   public SuperType(CharSequence field) {
      this.field = field;
   }

   public CharSequence getField() {
      return this.field;
   }

   public void setField(CharSequence field) {
      this.field = field;
   }
}

public class SubType extends SuperType {

   private String field;

   public SubType(String field) {
      super(field);
      this.field = field;
   }

   @Override
   public String getField() {
      return this.field;
   }

   public void setField(String field) {
      this.field = field;

      // optional
      super.setField(field);
   }
}

這兩個類都使用可賦值的型別定義了一個 field。然而,SubType 遮蔽了 SuperType.field。根據類設計,使用建構函式可能是設定 SuperType.field 的唯一預設方法。或者,在 setter 中呼叫 super.setField(…) 可以設定 SuperType 中的 field。所有這些機制都會在某種程度上產生衝突,因為屬性共享相同的名稱,但可能代表兩個不同的值。如果型別不可賦值,Spring Data 會跳過超型別屬性。也就是說,被覆蓋屬性的型別必須可賦值給其超型別屬性的型別,才能註冊為覆蓋,否則超型別屬性被視為瞬態(transient)。我們通常建議使用不同的屬性名稱。

Spring Data 模組通常支援持有不同值的被覆蓋屬性。從程式設計模型的角度來看,有幾點需要考慮:

  1. 哪些屬性應該被持久化(預設為所有宣告的屬性)?您可以透過使用 @Transient 註解來排除屬性。

  2. 如何在您的資料儲存中表示屬性?為不同值使用相同的欄位/列名通常會導致資料損壞,因此您應該至少為其中一個屬性使用顯式的欄位/列名進行註解。

  3. 不能使用 @AccessType(PROPERTY),因為在不對 setter 實現做任何進一步假設的情況下,通常無法設定超屬性。

Kotlin 支援

Spring Data 適應了 Kotlin 的特性,以允許物件建立和修改。

Kotlin 物件建立

支援例項化 Kotlin 類,所有類預設都是不可變的,並且需要顯式的屬性宣告來定義可變屬性。

Spring Data 會自動嘗試檢測用於例項化該型別物件的持久化實體的建構函式。解析演算法如下:

  1. 如果存在一個用 @PersistenceCreator 註解的建構函式,則使用它。

  2. 如果型別是 Kotlin data class,則使用其主建構函式。

  3. 如果存在一個用 @PersistenceCreator 註解的單一靜態工廠方法,則使用它。

  4. 如果只有一個建構函式,則使用它。

  5. 如果有多個建構函式並且只有一個用 @PersistenceCreator 註解,則使用它。

  6. 如果型別是 Java Record,則使用其規範建構函式。

  7. 如果存在無參建構函式,則使用它。其他建構函式將被忽略。

考慮以下 data class Person

data class Person(val id: String, val name: String)

上述類編譯成一個帶有顯式建構函式的典型類。我們可以透過新增另一個建構函式並使用 @PersistenceCreator 註解來指示建構函式偏好,從而自定義此類

data class Person(var id: String, val name: String) {

    @PersistenceCreator
    constructor(id: String) : this(id, "unknown")
}

Kotlin 透過允許在未提供引數時使用預設值來支援引數可選性。當 Spring Data 檢測到帶有引數預設值的建構函式時,如果資料儲存未提供值(或僅返回 null),它會省略這些引數,以便 Kotlin 可以應用引數預設值。考慮以下為 name 應用引數預設值的類

data class Person(var id: String, val name: String = "unknown")

每當 name 引數不是結果的一部分或其值為 null 時,name 將預設設定為 unknown

Spring Data 不支援委託屬性。對映元資料會過濾 Kotlin Data class 的委託屬性。在所有其他情況下,您可以透過使用 @Transient 註解屬性來排除委託屬性的合成欄位。

Kotlin data classes 的屬性填充

在 Kotlin 中,所有類預設都是不可變的,並且需要顯式的屬性宣告來定義可變屬性。考慮以下 data class Person

data class Person(val id: String, val name: String)

這個類實際上是不可變的。它允許建立新例項,因為 Kotlin 生成了一個 copy(…) 方法,該方法建立新的物件例項,複製現有物件的所有屬性值,並應用作為引數提供給方法的屬性值。

Kotlin 覆蓋屬性

Kotlin 允許宣告 屬性覆蓋 來改變子類中的屬性。

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
	SuperType(field) {
}

這樣的安排會產生兩個名為 field 的屬性。Kotlin 為每個類中的每個屬性生成屬性訪問器(getter 和 setter)。實際上,程式碼看起來如下

public class SuperType {

   private int field;

   public SuperType(int field) {
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

public final class SubType extends SuperType {

   private int field;

   public SubType(int field) {
      super(field);
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

SubType 上的 getter 和 setter 只設置 SubType.field,而不設定 SuperType.field。在這種安排下,使用建構函式是設定 SuperType.field 的唯一預設方法。向 SubType 新增一個方法透過 this.SuperType.field = … 來設定 SuperType.field 是可能的,但這超出了支援的約定範圍。屬性覆蓋在某種程度上會產生衝突,因為屬性共享相同的名稱,但可能代表兩個不同的值。我們通常建議使用不同的屬性名稱。

Spring Data 模組通常支援持有不同值的被覆蓋屬性。從程式設計模型的角度來看,有幾點需要考慮:

  1. 哪些屬性應該被持久化(預設為所有宣告的屬性)?您可以透過使用 @Transient 註解來排除屬性。

  2. 如何在您的資料儲存中表示屬性?為不同值使用相同的欄位/列名通常會導致資料損壞,因此您應該至少為其中一個屬性使用顯式的欄位/列名進行註解。

  3. 不能使用 @AccessType(PROPERTY),因為無法設定超屬性。

Kotlin 值類

Kotlin 值類旨在提供更具表達力的領域模型,以使底層概念更清晰。Spring Data 可以讀取和寫入使用值類定義屬性的型別。

考慮以下領域模型

@JvmInline
value class EmailAddress(val theAddress: String)                                    (1)

data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) (2)
1 一個帶有非 null 值型別的簡單值類。
2 使用 EmailAddress 值類定義屬性的資料類。
使用非原始值型別的非 null 屬性在編譯類中會扁平化為值型別。可 null 的原始值型別或可 null 的值中值型別則由其包裝型別表示,這會影響值型別在資料庫中的表示方式。

資料對映和型別轉換

本節解釋了型別如何對映到 Apache Cassandra 表示以及如何從 Apache Cassandra 表示中映射回來。

Spring Data for Apache Cassandra 支援 Apache Cassandra 提供的多種型別。除了這些型別之外,Spring Data for Apache Cassandra 還提供了一系列內建轉換器來對映其他型別。您可以提供自己的自定義轉換器來調整型別轉換。有關更多詳細資訊,請參見“使用自定義轉換器覆蓋預設對映”。下表將 Spring Data 型別對映到 Cassandra 型別

表 1. 型別
型別 Cassandra 型別

String

text (預設), varchar, ascii

double, Double

double

float, Float

float

long, Long

bigint (預設), counter

int, Integer

int

short, Short

smallint

byte, Byte

tinyint

boolean, Boolean

boolean

BigInteger

varint

BigDecimal

decimal

java.util.Date

timestamp

com.datastax.driver.core.LocalDate

date

InetAddress

inet

ByteBuffer

blob

java.util.UUID

uuid

TupleValue, 對映的 Tuple 型別

tuple<…>

UDTValue, 對映的使用者定義型別

user type

java.util.Map<K, V>

map

java.util.List<E>

list

java.util.Set<E>

set

Enum

text (預設), bigint, varint, int, smallint, tinyint

LocalDate
(Joda, Java 8, JSR310-BackPort)

date

LocalTime+ (Joda, Java 8, JSR310-BackPort)

time

LocalDateTime, LocalTime, Instant
(Joda, Java 8, JSR310-BackPort)

timestamp

ZoneId (Java 8, JSR310-BackPort)

text

每種支援的型別都對映到預設的 Cassandra 資料型別。Java 型別可以使用 @CassandraType 對映到其他 Cassandra 型別,如下例所示

示例 1. Enum 到數字型別的對映
@Table
public class EnumToOrdinalMapping {

  @PrimaryKey String id;

  @CassandraType(type = Name.INT) Condition asOrdinal;
}

public enum Condition {
  NEW, USED
}

基於約定的對映

當未提供額外對映元資料時,MappingCassandraConverter 使用一些約定來將領域物件對映到 CQL 表。約定如下:

  • 簡單的(短)Java 類名透過轉換為小寫來對映到表名。例如,com.bigbank.SavingsAccount 對映到名為 savingsaccount 的表。

  • 轉換器使用任何已註冊的 Spring Converter 例項來覆蓋物件屬性到表列的預設對映。

  • 物件的屬性用於與表中的列進行相互轉換。

您可以透過在 CassandraMappingContext 上配置 NamingStrategy 來調整約定。命名策略物件實現了從實體類和實際屬性派生表、列或使用者定義型別的約定。

下面的示例展示瞭如何配置 NamingStrategy

示例 2. 在 CassandraMappingContext 上配置 NamingStrategy
		CassandraMappingContext context = new CassandraMappingContext();

		// default naming strategy
		context.setNamingStrategy(NamingStrategy.INSTANCE);

		// snake_case converted to upper case (SNAKE_CASE)
		context.setNamingStrategy(NamingStrategy.SNAKE_CASE.transform(String::toUpperCase));

對映配置

除非明確配置,否則在建立 CassandraTemplate 時會預設建立一個 MappingCassandraConverter 例項。您可以建立自己的 MappingCassandraConverter 例項,以告訴它在啟動時掃描哪個 classpath 路徑來查詢您的領域類,從而提取元資料和構建索引。

此外,透過建立自己的例項,您可以註冊 Spring Converter 例項,用於將特定類與資料庫之間進行對映。以下示例配置類設定了 Cassandra 對映支援

示例 3. 配置 Cassandra 對映支援的 @Configuration 類
@Configuration
public class SchemaConfiguration extends AbstractCassandraConfiguration {

	@Override
	protected String getKeyspaceName() {
		return "bigbank";
	}

	// the following are optional

	@Override
	public CassandraCustomConversions customConversions() {

		return CassandraCustomConversions.create(config -> {
			config.registerConverter(new PersonReadConverter()));
			config.registerConverter(new PersonWriteConverter()));
		});
	}

	@Override
	public SchemaAction getSchemaAction() {
		return SchemaAction.RECREATE;
	}

	// other methods omitted...
}

AbstractCassandraConfiguration 要求您實現定義 keyspace 的方法。AbstractCassandraConfiguration 還有一個名為 getEntityBasePackages(…) 的方法。您可以覆蓋它,以告訴轉換器在哪裡掃描使用 @Table 註解的類。

您可以透過覆蓋 customConversions 方法向 MappingCassandraConverter 新增額外的轉換器。

AbstractCassandraConfiguration 建立一個 CassandraTemplate 例項,並以 cassandraTemplate 為名稱將其註冊到容器中。

基於元資料的對映

為了充分利用 Spring Data for Apache Cassandra 支援內部的物件對映功能,您應該使用 @Table 註解標註您的對映領域物件。這樣做可以讓 classpath 掃描器找到您的領域物件並進行預處理,以提取必要的元資料。只有被註解的實體才會用於執行 schema 操作。在最壞的情況下,SchemaAction.RECREATE_DROP_UNUSED 操作會刪除您的表,您將丟失資料。請注意,表是從會話的 keyspace 訪問的。但是,您可以指定自定義的 keyspace 來使用特定 keyspace 中的表/UDT。

以下示例展示了一個簡單的領域物件

示例 4. 領域物件示例
package com.mycompany.domain;

@Table
public class Person {

  @Id
  private String id;

  @CassandraType(type = Name.VARINT)
  private Integer ssn;

  private String firstName;

  private String lastName;
}
@Id 註解告訴對映器您希望將哪個屬性用作 Cassandra 的主鍵。複合主鍵可能需要稍微不同的資料模型。

使用主鍵

Cassandra 要求 CQL 表至少有一個分割槽鍵欄位。表還可以額外宣告一個或多個聚類鍵欄位。當您的 CQL 表具有複合主鍵時,您必須建立一個 @PrimaryKeyClass 來定義複合主鍵的結構。在此上下文中,“複合主鍵”是指一個或多個分割槽列與一個或多個聚類列的可選組合。

主鍵可以使用任何單一的簡單 Cassandra 型別或對映的使用者定義型別。不支援集合型別的主鍵。

簡單主鍵

簡單主鍵由實體類中的一個分割槽鍵欄位組成。由於它只有一個欄位,我們可以安全地假定它是一個分割槽鍵。以下清單顯示了在 Cassandra 中定義的 CQL 表,其主鍵為 user_id

示例 5. 在 Cassandra 中定義的 CQL 表
CREATE TABLE user (
  user_id text,
  firstname text,
  lastname text,
  PRIMARY KEY (user_id))
;

以下示例展示了一個 Java 類,其註解方式與上一個清單中定義的 Cassandra 表相對應

示例 6. 註解實體
@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey("user_id")
  private String userId;

  private String firstname;
  private String lastname;

  // getters and setters omitted

}

複合鍵

複合主鍵(或組合鍵)由多個主鍵欄位組成。也就是說,複合主鍵可以由多個分割槽鍵、一個分割槽鍵和一個聚類鍵,或者多個主鍵欄位組成。

複合鍵在 Spring Data for Apache Cassandra 中可以透過兩種方式表示:

  • 嵌入到實體中。

  • 透過使用 @PrimaryKeyClass

複合鍵最簡單的形式是包含一個分割槽鍵和一個聚類鍵。

以下示例展示了一個 CQL 語句來表示表及其複合鍵

示例 7. 帶有複合主鍵的 CQL 表
CREATE TABLE login_event(
  person_id text,
  event_code int,
  event_time timestamp,
  ip_address text,
  PRIMARY KEY (person_id, event_code, event_time))
  WITH CLUSTERING ORDER BY (event_time DESC)
;

平坦複合主鍵

平坦複合主鍵作為平坦欄位嵌入到實體內部。主鍵欄位使用 @PrimaryKeyColumn 進行註解。選擇需要查詢包含針對各個欄位的謂詞,或者使用 MapId。以下示例展示了一個帶有平坦複合主鍵的類

示例 8. 使用平坦複合主鍵
@Table(value = "login_event")
class LoginEvent {

  @PrimaryKeyColumn(name = "person_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
  private String personId;

  @PrimaryKeyColumn(name = "event_code", ordinal = 1, type = PrimaryKeyType.PARTITIONED)
  private int eventCode;

  @PrimaryKeyColumn(name = "event_time", ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
  private LocalDateTime eventTime;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}

主鍵類

主鍵類是對映到實體多個欄位或屬性的複合主鍵類。它使用 @PrimaryKeyClass 進行註解,並且應該定義 equalshashCode 方法。這些方法的值相等性語義應與主鍵對映到的資料庫型別的資料庫相等性一致。主鍵類可以與 repositories 一起使用(作為 Id 型別),並用於在單個複雜物件中表示實體的身份。以下示例展示了一個複合主鍵類

示例 9. 複合主鍵類
@PrimaryKeyClass
class LoginEventKey implements Serializable {

  @PrimaryKeyColumn(name = "person_id", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
  private String personId;

  @PrimaryKeyColumn(name = "event_code", ordinal = 1, type = PrimaryKeyType.PARTITIONED)
  private int eventCode;

  @PrimaryKeyColumn(name = "event_time", ordinal = 2, type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
  private LocalDateTime eventTime;

  // other methods omitted
}

以下示例展示瞭如何使用複合主鍵

示例 10. 使用複合主鍵
@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey
  private LoginEventKey key;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}

嵌入實體支援

嵌入式實體用於在您的Java領域模型中設計值物件,其屬性會被展平到表中。在下面的示例中,您可以看到User.name使用@Embedded進行了註解。其結果是UserName的所有屬性都被摺疊到user表中,該表包含3列(user_idfirstnamelastname)。

嵌入式實體只能包含簡單的屬性型別。無法將一個嵌入式實體巢狀到另一個嵌入式實體中。

然而,如果在結果集中firstnamelastname的列值實際上是null,則根據@EmbeddedonEmpty屬性,整個name屬性將被設定為null。當所有巢狀屬性都為null時,onEmpty屬性將物件設定為null
與此行為相反,USE_EMPTY會嘗試使用預設建構函式或接受結果集中可空引數值的建構函式來建立新例項。

示例 11. 嵌入物件的示例程式碼
public class User {

	@PrimaryKey("user_id")
    private String userId;

    @Embedded(onEmpty = USE_NULL) (1)
    UserName name;
}

public class UserName {
    private String firstname;
    private String lastname;
}
1 如果firstnamelastnamenull,則屬性為null。使用onEmpty=USE_EMPTY來例項化UserName,其屬性可能為null值。

您可以透過使用@Embedded註解的可選prefix元素在實體中多次嵌入值物件。該元素表示一個字首,並會新增到嵌入物件中每個列名的前面。請注意,如果多個屬性渲染到相同的列名,則屬性會相互覆蓋。

利用快捷方式@Embedded.Nullable@Embedded.Empty分別替代@Embedded(onEmpty = USE_NULL)@Embedded(onEmpty = USE_EMPTY),以減少冗長性,並同時相應地設定JSR-305 @javax.annotation.Nonnull

public class MyEntity {

    @Id
    Integer id;

    @Embedded.Nullable (1)
    EmbeddedEntity embeddedEntity;
}
1 @Embedded(onEmpty = USE_NULL)的快捷方式。

對映註解概述

MappingCassandraConverter可以使用元資料來驅動物件到Cassandra表中行的對映。下面是註解的概述

  • @Id: 應用於欄位或屬性級別,用於標記用於標識目的的屬性。

  • @Table: 應用於類級別,表示該類是對映到資料庫的候選類。您可以指定儲存物件的表的名稱。指定keyspace時,表名將在所有DML和DDL操作中以字首keyspace。

  • @PrimaryKey: 類似於@Id,但允許您指定列名。

  • @PrimaryKeyColumn: Cassandra特有的主鍵列註解,允許您指定主鍵列屬性,例如用於 clustered 或 partitioned。可用於單個或多個屬性,表示單個或複合(複合)主鍵。如果在實體內的屬性上使用此註解,請確保也應用@Id註解。

  • @PrimaryKeyClass: 應用於類級別,表示該類是複合主鍵類。必須在實體類中使用@PrimaryKey引用。

  • @Transient: 預設情況下,所有私有欄位都對映到行。此註解排除了應用它的欄位,使其不儲存在資料庫中。瞬時屬性不能用於持久化建構函式中,因為轉換器無法為建構函式引數具體化值。

  • @PersistenceConstructor: 標記給定的建構函式(即使是包保護的)以便從資料庫例項化物件時使用。建構函式引數透過名稱對映到檢索到的行中的鍵值。

  • @Value: 此註解是Spring Framework的一部分。在對映框架中,它可以應用於建構函式引數。這允許您使用Spring Expression Language語句來轉換在資料庫中檢索到的鍵的值,然後再將其用於構造領域物件。為了引用給定Row/UdtValue/TupleValue的屬性,必須使用類似於@Value("#root.getString(0)")的表示式,其中root指代給定文件的根。

  • @ReadOnlyProperty: 應用於欄位級別,將屬性標記為只讀。實體相關的插入和更新語句不包含此屬性。

  • @Column: 應用於欄位級別。描述了在Cassandra表中表示的列名,從而允許名稱與類的欄位名不同。可用於建構函式引數,以便在建構函式建立期間自定義列名。

  • @Embedded: 應用於欄位級別。允許對對映到表或使用者定義型別的型別使用嵌入式物件。嵌入式物件的屬性會展平到其父結構的結構中。

  • @Indexed: 應用於欄位級別。描述在會話初始化時建立的索引。

  • @SASI: 應用於欄位級別。允許在會話初始化期間建立SASI索引。

  • @CassandraType: 應用於欄位級別,指定Cassandra資料型別。型別預設從屬性宣告中派生。

  • @Frozen: 應用於類型別和引數化型別的欄位級別。宣告一個凍結的UDT列或凍結集合,例如List<@Frozen UserDefinedPersonType>

  • @UserDefinedType: 應用於型別級別,指定Cassandra使用者定義資料型別(UDT)。指定keyspace時,UDT名稱將在所有DML和DDL操作中以字首keyspace。型別預設從宣告中派生。

  • @Tuple: 應用於型別級別,將型別用作對映的元組。

  • @Element: 應用於欄位級別,指定對映元組中的元素或欄位序號。型別預設從屬性宣告中派生。可用於建構函式引數,以便在建構函式建立期間自定義元組元素序號。

  • @Version: 應用於欄位級別,用於樂觀鎖定,並在儲存操作時檢查修改。初始值為zero,每次更新時會自動增加。

對映元資料基礎設施定義在單獨的spring-data-commons專案中,該專案獨立於技術和資料儲存。

以下示例顯示了更復雜的對映

示例 12. 對映的Person
@Table("my_person")
public class Person {

	@PrimaryKeyClass
	public static class Key implements Serializable {

		@PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED)
		private String type;

		@PrimaryKeyColumn(ordinal = 1, type = PrimaryKeyType.PARTITIONED)
		private String value;

		@PrimaryKeyColumn(name = "correlated_type", ordinal = 2, type = PrimaryKeyType.CLUSTERED)
		private String correlatedType;

		// other getters/setters omitted
	}

	@PrimaryKey
	private Person.Key key;

	@CassandraType(type = CassandraType.Name.VARINT)
	private Integer ssn;

	@Column("f_name")
	private String firstName;

	@Column
	@Indexed
	private String lastName;

	private Address address;

	@CassandraType(type = CassandraType.Name.UDT, userTypeName = "myusertype")
	private UdtValue usertype;

	private Coordinates coordinates;

	@Transient
	private Integer accountTotal;

	@CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.BIGINT)
	private Set<Long> timestamps;

	private Map<@Indexed String, InetAddress> sessions;

	public Person(Integer ssn) {
		this.ssn = ssn;
	}

	public Person.Key getKey() {
		return key;
	}

	// no setter for Id.  (getter is only exposed for some unit testing)

	public Integer getSsn() {
		return ssn;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	// other getters/setters omitted
}

以下示例顯示瞭如何對映UDT Address

示例 13. 對映的使用者定義型別Address
@UserDefinedType("address")
public class Address {

  @CassandraType(type = CassandraType.Name.VARCHAR)
  private String street;

  private String city;

  private Set<String> zipcodes;

  @CassandraType(type = CassandraType.Name.SET, typeArguments = CassandraType.Name.BIGINT)
  private List<Long> timestamps;

  // other getters/setters omitted
}
使用使用者定義型別需要一個配置了對映上下文的UserTypeResolver。有關如何配置UserTypeResolver,請參見配置章節

以下示例顯示瞭如何對映元組

示例 14. 對映的元組
@Tuple
class Coordinates {

  @Element(0)
  @CassandraType(type = CassandraType.Name.VARCHAR)
  private String description;

  @Element(1)
  private long longitude;

  @Element(2)
  private long latitude;

  // other getters/setters omitted
}

索引建立

如果您希望在應用程式啟動時建立二級索引,可以使用@Indexed@SASI註解特定實體屬性。索引建立為標量型別、使用者定義型別和集合型別建立簡單的二級索引。

您可以配置SASI索引來應用分析器,例如StandardAnalyzerNonTokenizingAnalyzer(分別使用@StandardAnalyzed@NonTokenizingAnalyzed)。

對映型別區分ENTRYKEYSVALUES索引。索引建立從被註解的元素派生索引型別。以下示例顯示了建立索引的多種方式

示例 15. 對映索引的變體
@Table
class PersonWithIndexes {

  @Id
  private String key;

  @SASI
  @StandardAnalyzed
  private String names;

  @Indexed("indexed_map")
  private Map<String, String> entries;

  private Map<@Indexed String, String> keys;

  private Map<String, @Indexed String> values;

  // …
}

@Indexed註解可以應用於嵌入式實體的單個屬性,或者與@Embedded註解一起使用,在這種情況下,嵌入式物件的所有屬性都會被索引。

在會話初始化時建立索引可能會對應用程式啟動效能產生嚴重影響。