映射

丰富的对象映射支持由 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注解一起使用,在这种情况下,嵌入式对象的所有属性都会被索引。

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