Mapping

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;
  }
}
產生的屬性存取器
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 Runtime 必須允許在原始 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 方法。如果您需要這些方法,請盡可能將它們設為套件保護,以便它們只能由有限數量的共置類型叫用。僅使用建構子的實例化比屬性填充快達 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 會跳過超類型屬性。也就是說,覆寫屬性的類型必須可指派給其超類型屬性類型才能註冊為覆寫,否則超類型屬性將被視為暫時屬性。我們通常建議使用不同的屬性名稱。

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

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

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

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

Kotlin 支援

Spring Data 調整 Kotlin 的特性,以允許物件建立和變更。

Kotlin 物件建立

支援實例化 Kotlin 類別,所有類別預設都是不可變的,並且需要明確的屬性宣告才能定義可變屬性。

Spring Data 自動嘗試偵測要用於實例化該類型物件的持久實體建構子。解析演算法運作方式如下

  1. 如果存在以 @PersistenceCreator 註解的建構子,則使用該建構子。

  2. 如果類型是 Kotlin 資料類別,則使用主要建構子。

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

  4. 如果存在單一建構子,則使用該建構子。

  5. 如果存在多個建構子,且只有一個以 @PersistenceCreator 註解,則使用該建構子。

  6. 如果類型是 Java Record,則使用標準建構子。

  7. 如果存在無引數建構子,則使用該建構子。其他建構子將被忽略。

考慮以下 data 類別 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 資料類別的委派屬性。在所有其他情況下,您可以透過使用 @delegate:org.springframework.data.annotation.Transient 註解屬性,來排除委派屬性的合成欄位。

Kotlin 資料類別的屬性填充

在 Kotlin 中,所有類別預設都是不可變的,並且需要明確的屬性宣告才能定義可變屬性。考慮以下 data 類別 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 Value Classes

Kotlin Value Classes 旨在提供更具表達力的網域模型,以明確底層概念。Spring Data 可以讀取和寫入使用 Value Classes 定義屬性的類型。

考慮以下網域模型

@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 表示法之間相互對應。

Spring Data for Apache Cassandra 支援 Apache Cassandra 提供的幾種類型。除了這些類型之外,Spring Data for Apache Cassandra 還提供一組內建轉換器來對應其他類型。您可以提供自己的自訂轉換器來調整類型轉換。如需更多詳細資訊,請參閱「使用自訂轉換器覆寫預設對應」。下表將 Spring Data 類型對應到 Cassandra 類型

表 1. 類型
類型 Cassandra 類型

String

text(預設)、varcharascii

doubleDouble

double

floatFloat

float

longLong

bigint(預設)、counter

intInteger

int

shortShort

smallint

byteByte

tinyint

booleanBoolean

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(預設)、bigintvarintintsmallinttinyint

LocalDate
(Joda、Java 8、JSR310-BackPort)

date

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

time

LocalDateTimeLocalTimeInstant
(Joda、Java 8、JSR310-BackPort)

timestamp

ZoneId (Java 8、JSR310-BackPort)

text

每個支援的類型都對應到預設的Cassandra 資料類型。Java 類型可以使用 @CassandraType 對應到其他 Cassandra 類型,如下列範例所示

範例 1. 列舉對應到數值類型
@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 實例,以告知它在啟動時掃描類別路徑以尋找您的網域類別,從而擷取中繼資料並建構索引。

此外,透過建立自己的實例,您可以註冊 Spring Converter 實例,以用於將特定類別對應到資料庫和從資料庫對應而來。以下範例配置類別設定了 Cassandra 對應支援

範例 3. @Configuration 類別,用於配置 Cassandra 對應支援
@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 要求您實作定義鍵空間的方法。AbstractCassandraConfiguration 也有一個名為 getEntityBasePackages(…) 的方法。您可以覆寫它,以告知轉換器在哪裡掃描以尋找以 @Table 註解的類別。

您可以透過覆寫 customConversions 方法,將其他轉換器新增至 MappingCassandraConverter

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

基於中繼資料的對應

為了充分利用 Spring Data for Apache Cassandra 支援內部的物件對應功能,您應使用 @Table 註解來註解您對應的網域物件。這樣做可讓類別路徑掃描器尋找並預先處理您的網域物件,以擷取必要的中繼資料。只有註解的實體用於執行綱要動作。在最壞的情況下,SchemaAction.RECREATE_DROP_UNUSED 操作會刪除您的資料表,而您會遺失資料。請注意,資料表是從會話鍵空間存取的。但是,您可以指定自訂鍵空間,以使用來自特定鍵空間的資料表/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 方法。這些方法的數值相等語義應與金鑰映射到的資料庫類型的資料庫相等性一致。主鍵類別可以與儲存庫(作為 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 嘗試使用預設建構子或接受來自結果集的可為 null 參數值的建構子來建立新實例。

範例 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 特有的主鍵資料行註解,可讓您指定主鍵資料行屬性,例如用於叢集或分割。可用於單一和多個屬性,以指示單一或複合(複合)主鍵。如果在實體內的屬性上使用,請務必同時應用 @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)。

Map 類型區分 ENTRYKEYSVALUES 索引。索引建立會從註解元素衍生索引類型。以下範例顯示多種建立索引的方式

範例 15. Map 索引的不同變體
@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 註解一起使用,在後者的情況下,嵌入式的所有屬性都會被索引。

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