對映

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.lastname);
  }

  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 執行時必須允許在原始 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. 不能使用 @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. 如果存在無引數建構函式,則使用它。其他建構函式將被忽略。

考慮以下 dataPerson

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 類中的委託屬性。在所有其他情況下,您可以透過使用 @Transient 註解屬性來排除委託屬性的合成欄位。

Kotlin 資料類的屬性填充

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

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 一個帶有非空值型別的簡單值類。
2 使用 EmailAddress 值類定義屬性的資料類。
使用非原始值型別的非空屬性在編譯後的類中被展平為值型別。可空原始值型別或可空值中值型別用其包裝器型別表示,這會影響值型別在資料庫中的表示方式。

資料對映和型別轉換

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

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

表 1. 型別
型別 Cassandra 型別

字串

text(預設)、varcharascii

doubleDouble

double

floatFloat

float

longLong

bigint(預設)、counter

intInteger

int

shortShort

smallint

byteByte

tinyint

booleanBoolean

布林值

BigInteger

varint

BigDecimal

decimal

java.util.Date

timestamp

com.datastax.driver.core.LocalDate

date

InetAddress

inet

ByteBuffer

blob

java.util.UUID

uuid

TupleValue,對映的元組型別

tuple<…>

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

user type

Vector, CqlVector

vector<…>

java.util.Map<K, V>

map

java.util.List<E>

list

java.util.Set<E>

set

列舉

text(預設),bigintvarintintsmallinttinyint

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. 列舉對映到數值型別
@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. 配置 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 要求您實現定義鍵空間的方法。 AbstractCassandraConfiguration 還有一個名為 getEntityBasePackages(…) 的方法。您可以覆蓋它以告訴轉換器在哪裡掃描帶有 @Table 註解的類。

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

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

基於元資料的對映

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

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

示例 4. 示例領域物件
@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,則整個屬性 name 將根據 @EmbeddedonEmpty 設定為 null,當所有巢狀屬性都為 null 時,它將物件設定為 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:應用於類級別,表示此類是對映到資料庫的候選。您可以指定儲存物件的表的名稱。指定鍵空間時,表名將在所有 DML 和 DDL 操作中以鍵空間為字首。

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

  • @PrimaryKeyColumn:Cassandra 特有的主鍵列註解,允許您指定主鍵列屬性,例如用於叢集或分割槽。可用於單個和多個屬性,以指示單個或複合(組合)主鍵。如果在實體內的屬性上使用,請確保同時應用 @Id 註解。

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

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

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

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

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

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

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

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

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

  • @SaiIndexed:應用於欄位級別。允許在會話初始化期間定義 SAI(儲存附加索引)索引建立。

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

  • @VectorType:應用於欄位級別以指定 Cassandra 向量型別。使用模式生成時需要此註解。

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

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

  • @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@SaiIndexed@SASI 註解特定的實體屬性。索引建立為標量型別、使用者定義型別和集合型別建立簡單的二級索引。

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

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

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

  @Id
  private String key;

  @SASI
  @StandardAnalyzed
  private String names;

  @SaiIndexed
  @VectorType(dimensions = 1536) // required for table generation
  private Vector vector;

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

  private Map<@Indexed String, String> keys;

  private Map<String, @Indexed String> values;

  // …
}

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

在會話初始化時建立索引可能會對應用程式啟動產生嚴重效能影響。
© . This site is unofficial and not affiliated with VMware.