映射

MappingCassandraConverter 提供了丰富的对象映射支持。MappingCassandraConverter 拥有丰富的元数据模型,提供了一套完整的功能集,用于将领域对象映射到 CQL 表。

映射元数据模型是通过在领域对象上使用注解来填充的。但是,该基础设施并不局限于仅使用注解作为元数据的唯一来源。MappingCassandraConverter 还允许您在不提供任何额外元数据的情况下,通过遵循一组约定将领域对象映射到表。

在本节中,我们将描述 MappingCassandraConverter 的功能,如何使用约定将领域对象映射到表,以及如何使用基于注解的映射元数据来覆盖这些约定。

对象映射基础

本节涵盖了 Spring Data 对象映射的基础知识,包括对象创建、字段和属性访问、可变性和不可变性。请注意,本节仅适用于不使用底层数据存储的对象映射的 Spring Data 模块(如 JPA)。还要确保查阅特定存储的章节,了解特定存储的对象映射,例如索引、自定义列或字段名称等。

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运行时必须允许在原始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实例保持不变,因为创建了一个新的实例。通常将相同的模式应用于其他由存储管理但可能需要更改才能执行持久性操作的属性。with方法是可选的,因为持久化构造函数(参见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数据类,则使用主构造函数。

  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数据类的委托属性。在所有其他情况下,您可以通过使用@delegate:org.springframework.data.annotation.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 类型

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,映射的用户定义类型

用户类型

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

LocalDateTimeLocalTimeInstant
(Joda、Java 8、JSR310-BackPort)

timestamp

ZoneId(Java 8、JSR310-BackPort)

text

每个受支持的类型都映射到一个默认的Cassandra 数据类型。可以使用@CassandraType将 Java 类型映射到其他 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 实例以用于将特定类映射到数据库以及从数据库映射。

示例 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. 示例域对象
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 中定义的具有 user_id 主键的 CQL 表

示例 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,当所有嵌套属性都为 null 时,整个属性 name 将设置为 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 使用其属性的潜在 null 值实例化 UserName

您可以通过使用 @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 索引。

  • @CassandraType:应用于字段级别以指定 Cassandra 数据类型。类型默认情况下是从属性声明派生的。

  • @Frozen:应用于字段级别的类类型和参数化类型。声明一个冻结的 UDT 列或冻结的集合,例如List<@Frozen UserDefinedPersonType>

  • @UserDefinedType:应用于类型级别以指定 Cassandra 用户定义的数据类型 (UDT)。在指定 keyspace 时,UDT 名称将在所有 DML 和 DDL 操作中以 keyspace 为前缀。类型默认情况下是从声明派生的。

  • @Tuple:应用于类型级别,将类型用作映射的元组。

  • @Element:应用于字段级别,以指定映射元组中的元素或字段序数。类型默认情况下是从属性声明派生的。可以在构造函数参数上使用它来在构造函数创建期间自定义元组元素序数。

  • @Version:应用于字段级别,用于乐观锁,并在保存操作中检查修改。初始值为,每次更新都会自动递增。

映射元数据基础结构是在独立的 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注解一起使用,在这种情况下,将对嵌入式的所有属性进行索引。

在会话初始化时创建索引可能会严重影响应用程序启动的性能。