Mapping

MappingCassandraConverter 提供丰富的对象映射支持。MappingCassandraConverter 拥有丰富的元数据模型,该模型提供了将域对象映射到 CQL 表的功能的完整特性集。 映射元数据模型通过使用域对象上的注释来填充。然而,该基础结构并不限制将注释用作唯一元数据源。通过遵循一系列约定,MappingCassandraConverter 还允许您将域对象映射到表,而无需提供任何其他元数据。 在本章中,我们将描述 MappingCassandraConverter 的特性、如何使用约定将域对象映射到表,以及如何使用基于注释的映射元数据覆盖这些约定。

Object Mapping Fundamentals

本节涵盖了 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不可变性的基础知识。请注意,此部分只适用于未使用基础数据存储的对象映射(如 JPA)的 Spring Data 模块。还要务必查阅特定于存储的部分,了解特定于存储的对象映射,如索引、自定义列或字段名称等。 Spring Data 对象映射的核心职责是创建域对象实例并将存储本地数据结构映射到这些实例上。这意味着我们需要两个基本步骤:

  1. 通过使用公开的其中一个构造函数创建实例。

  2. 实例填充以实现所有公开的属性。

Object creation

Spring Data 自动尝试检测一个持久实体的构造函数,以便用于实现该类型的对象。解决算法的工作方式如下:

  1. 如果有使用 @PersistenceCreator 注释的单个静态工厂方法,则使用该方法。

  2. 如果有单个构造函数,则使用该构造函数。

  3. 如果有多个构造函数,并且只有一个使用 @PersistenceCreator 注释,则使用该构造函数。

  4. 如果该类型是一个 Java Record,则使用规范构造函数。

  5. 如果有无参数构造函数,则会使用它。将忽略其他构造函数。

值解析假定构造函数/工厂方法的参数名称与实体的属性名称匹配,即解析将执行得好像要填充该属性一样,包括映射中的所有定制(不同的数据存储列或字段名称等)。这也要求在类文件或构造函数上有 @ConstructorProperties 注释中提供参数名称信息。

可以通过使用 Spring Framework 的 @Value 值注释来使用特定于存储的 SpEL 表达式来定制值解析。请查阅有关具体存储的映射的部分以获取更多详细信息。

Object creation internals

为了避免反射时间开销,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 将回退到通过反射进行实体实例化。

Property population

在创建了该实体的一个实例之后,Spring Data 填充该类的所有剩余的持久属性。除非已通过实体的构造函数填充(即通过其构造函数参数列表消耗),否则将首先填充标识符属性,以允许解析循环对象引用。在此之后,所有尚未被构造函数填充的非瞬态属性都会在实体实例上设置。为此,我们使用以下算法:

  1. 如果属性为不可变但公开 with… 方法(如下所示),我们将使用 with… 方法创建具有新属性值的新实体实例。

  2. 如果定义了属性访问(即通过 getter 和 setter 访问),我们将调用 setter 方法。

  3. 如果属性为可变的,我们将直接设置字段。

  4. 如果属性不可变,我们将使用持久化操作中使用的构造函数(见 Object creation)创建实例的副本。

  5. 默认情况下,将直接设置字段值。

Property population internals

类似于我们在对象构造中的优化,映射.object-creation.details,我们还使用 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;
  }
}
A generated 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 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的属性访问器。

让我们看看以下实体:

A sample entity
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 标识符属性为最终值,但在构造函数中将其设置为 null。类公开一个 withId(…) 方法,该方法用于设置标识符,例如,当将一个实例插入到数据存储中并且生成了一个标识符时。原始 Person 实例保持不变,因为创建了一个新的实例。通常将相同的模式应用于其他由存储管理但可能必须更改为持久化操作的属性。wither 方法是可选的,因为持久化构造函数(见 6)实际上是一个复制构造函数,并且设置属性将转换为创建一个应用了新标识符值的新实例。
2 firstnamelastname 属性是可通过 Getter 公开的简单不可变属性。
3 age 属性是不可变但从 birthday 属性派生的属性。采用所示设计,数据库值将胜过默认值,这是因为 Spring Data 仅使用声明的构造函数。即使目的是计算优先,但此构造函数也必须将 age 作为参数(以潜在忽略它),否则属性填充步骤将尝试设置年龄字段并因为该字段不可变且没有存在 with… 方法而失败。
4 comment 属性是可变的,通过直接设置其字段进行填充。
5 remarks 属性是可变的,通过调用赋值器方法进行填充。
6 类公开了一个工厂方法和一个用于创建对象的构造函数。此处的核心概念是使用工厂方法,而不是附加构造函数,以避免必须通过 @PersistenceCreator 消除构造函数二义性。在工厂方法中处理属性的默认值。如果您想让 Spring Data 使用工厂方法实例化对象,请使用 @PersistenceCreator 为其添加注释。

General recommendations

  • Try to stick to immutable objects — 不可变对象易于创建,因为具体化一个对象就是调用其构造函数。此外,这避免在您的领域对象中布满通过设置器方法允许客户端代码处理对象状态的情况。如果您需要这些代码,请使用程序包保护它们,以便只能通过有限的同置类型调用它们。仅构造函数具体化比属性填充快 30%。

  • Provide an all-args constructor — 即使您不能或不想将实体建模为不可变值,提供一个构造函数(它将所有实体属性作为参数,包括可变属性)仍然有价值,因为这允许对象映射跳过属性填充,以获得最佳性能。

  • Use factory methods instead of overloaded constructors to avoid `@PersistenceCreator` — 为了获得最佳性能,通常需要一个所有参数构造函数,我们通常希望公开更多应用用例特定的构造函数,这些构造函数省略自动生成标识符等信息。一个成熟的模式是使用静态工厂方法公开所有参数构造函数的这些变体。

  • Make sure you adhere to the constraints that allow the generated instantiator and property accessor classes to be used —

  • For identifiers to be generated, still use a final field in combination with an all-arguments persistence constructor (preferred) or a with… method —

  • Use Lombok to avoid boilerplate code — 由于持久化操作通常需要一个获取所有参数的构造函数,因此,它们的声明会变成枯燥地重复参数到字段赋值的样板,而使用 Lombok 的 @AllArgsConstructor 可以很好地避免这种情况。

Overriding Properties

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 的唯一默认方式。或者,在设置器中调用 super.setField(…) 可以设置 SuperType 中的 field 。由于这些属性共享相同名称,但可能表示两个不同的值,所有这些机制在某种程度上都会产生冲突。如果类型不可分配,Spring Data 将跳过超类型属性。也就是说,被重写的属性的类型必须可以分配给其超类型属性类型才能被注册为覆盖项,否则超类型属性被视为瞬态。我们通常建议使用不同的属性名称。

Spring Data 模块普遍支持包含不同值的重写属性。从编程模型的角度来看,有几件事需要考虑:

  1. 哪种属性应该持久化(默认为所有已声明的属性)?您可以添加 @Transient 注释到属性上以排除它们。

  2. 如何在数据存储中表示属性?不同值使用相同的字段/列名称通常会导致数据损坏,因此您应该至少使用一个显式字段/列名称为属性添加注释。

  3. 由于不能在不针对赋值器实现做出进一步假设的情况下设置超属性,所以不能使用 @AccessType(PROPERTY)

Kotlin support

Spring Data 适应 Kotlin 的特殊性,允许创建和更改对象。

Kotlin object creation

支持实例化 Kotlin 类,所有类在默认情况下都是不可变的,需要显式属性声明才能定义可变属性。

Spring Data 自动尝试检测一个持久实体的构造函数,以便用于实现该类型的对象。解决算法的工作方式如下:

  1. 如果有一个带 @PersistenceCreator 注释的构造函数,则使用它。

  2. 如果类型是 Kotlin data cass,则使用主构造函数。

  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

Property population of Kotlin data classes

在 Kotlin 中,所有类默认情况下是不可变的,需要显式声明属性来定义可变属性。考虑下面的 dataPerson

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

这个类实际上是不可变的。它允许创建新的实例,因为 Kotlin 生成了一个 copy(…) 方法,该方法创建新的对象实例,复制所有属性值(从现有对象)并将作为参数提供给该方法的属性值应用进来。

Kotlin Overriding Properties

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 值类被设计用于更具表现力的领域模型来使基础概念明确化。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 值类定义属性。

在已编译类中,使用非基本值类型而非空属性会展平为该值类型。可空基本值类型或可空值中值类型将用包装器类型表示,并影响在数据库中如何表示值类型。

Data Mapping and Type Conversion

本节说明了如何将类型映射到 Apache Cassandra 表示形式,以及如何从 Apache Cassandra 表示形式映射类型。

Apache Cassandra 的 Spring Data 支持 Apache Cassandra 提供的多种类型。除了这些类型之外,Apache Cassandra 的 Spring Data 还提供了一组内置转换器来映射其他类型。您可以提供自己的自定义转换器来调整类型转换。有关更多详细信息,请参见 “Overriding Default Mapping with Custom Converters”。下表将 Spring Data 类型映射到 Cassandra 类型:

Table 1. Type
Type Cassandra types

String

text (default), varchar, ascii

double, Double

double

float, Float

float

long, Long

bigint (default), 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, mapped Tuple Types

tuple<…>

UDTValue, mapped User-Defined Types

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

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

timestamp

ZoneId (Java 8, JSR310-BackPort)

text

每种受支持的类型映射到一个默认 Cassandra data type。通过使用 @CassandraType,Java 类型可以映射到其他 Cassandra 类型,如下面的示例所示:

Example 1. Enum mapping to numeric types
@Table
public class EnumToOrdinalMapping {

  @PrimaryKey String id;

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

public enum Condition {
  NEW, USED
}

Convention-based Mapping

当不提供其他映射元数据时,MappingCassandraConverter 使用一些约定将域对象映射到 CQL 表。这些约定是:

  • 简单的(短的)Java 类名称通过转换为小写形式而映射到表名。例如,com.bigbank.SavingsAccount 映射到名为 savingsaccount 的表。

  • 转换器使用任何已注册的 Spring Converter 实例来覆盖对象属性到表列的默认映射。

  • 对象的属性用于转换到表中的列以及从表中的列转换。

您可以通过在 CassandraMappingContext 上配置 NamingStrategy 来调整约定。命名策略对象实现了根据实体类和实际属性派生表、列或用户定义类型的约定。

以下示例演示如何配置 NamingStrategy

Example 2. Configuring NamingStrategy on CassandraMappingContext
/*
 * Copyright 2020-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.cassandra.example;

import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
import org.springframework.data.cassandra.core.mapping.NamingStrategy;

class NamingStrategyConfiguration {

	public void configurationMethod() {

		// tag::method[]
		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));

		// end::method[]
	}
}

Mapping Configuration

在创建 CassandraTemplate 时,除非显式配置,否则默认会创建一个 MappingCassandraConverter 实例。您可以创建自己的 MappingCassandraConverter 实例,以告诉它在启动时在类路径的何处扫描域类,以提取元数据和构建索引。

此外,通过创建自己的实例,您可以注册 Spring Converter 实例,以便用于将特定类映射到数据库中并再从中映射出来。以下示例配置类设置了 Cassandra 映射支持:

Example 3. @Configuration class to configure Cassandra mapping support
/*
 * Copyright 2020-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.cassandra.example;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.cassandra.config.AbstractCassandraConfiguration;
import org.springframework.data.cassandra.config.SchemaAction;
import org.springframework.data.cassandra.core.convert.CassandraCustomConversions;

// tag::class[]
@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...
}
// end::class[]

AbstractCassandraConfiguration 要求您实现定义 keyspace 的方法。AbstractCassandraConfiguration 还有一个名为 getEntityBasePackages(…​) 的方法。您可以覆盖它来告诉转换器在何处扫描带有 @Table 注释的类。

可以通过覆盖 customConversions 方法向 MappingCassandraConverter 添加其他转换器。

AbstractCassandraConfiguration 创建一个 CassandraTemplate 实例,并以 cassandraTemplate 的名称将其注册到容器中。

Metadata-based Mapping

要充分利用 Spring Data for Apache Cassandra 支持中的对象映射功能,您应该使用 @Table 注释注释您的映射域对象。这样操作可以让类路径扫描器找到并预处理域对象,以提取必要的元数据。只有带注释的实体用于执行模式操作。在最坏的情况下,SchemaAction.RECREATE_DROP_UNUSED 操作会删除您的表,并且您会丢失数据。以下示例显示了一个简单的域对象:

Example 4. Example domain object
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 主键的属性。复合主键可能需要略有不同的数据模型。

Working with Primary Keys

Cassandra 要求 CQL 表至少有一个分区键字段。此外,一个表还可以声明一个或多个聚集键字段。当您的 CQL 表具有复合主键时,您必须创建一个 @PrimaryKeyClass 来定义复合主键的结构。在此上下文中,“`复合主键`" 意味着一个或多个分区列,这些分区列可以与一个或多个聚集列相结合(可选)。

主键可以使用任何单一简单 Cassandra 类型或映射的用户定义类型。不支持集合类型的关键字。

Simple Primary Keys

简单主键由一个实体类中的一个分区键字段组成。因为它只有一个字段,所以我们可以安全地认为它是一个分区键。以下列表显示了在 Cassandra 中定义带有 user_id 主键的 CQL 表:

Example 5. CQL Table defined in Cassandra
CREATE TABLE user (
  user_id text,
  firstname text,
  lastname text,
  PRIMARY KEY (user_id))
;

以下示例展示了一个 Java 类,在使用注解的情况下,它与前面列表中定义的 Cassandra 相对应:

Example 6. Annotated Entity
@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey("user_id")
  private String userId;

  private String firstname;
  private String lastname;

  // getters and setters omitted

}

Composite Keys

复合主键(或复合键)由多个主键字段组成。也就是说,一个复合主键可以由多个分区键、一个分区键加一个集群键或一组主键字段组成。

使用适用于 Apache Cassandra 的 Spring Data 可以用两种方式表示复合键:

  • Embedded in an entity.

  • By using @PrimaryKeyClass.

复合键最简单的形式是带有分区键和集群键的键。

以下示例展示一个 CQL 语句以表示表及其复合键:

Example 7. CQL Table with a Composite Primary Key
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)
;

Flat Composite Primary Keys

扁平复合主键作为扁平字段嵌入实体中。主键字段使用 “@PrimaryKeyColumn” 进行注解。选择需要一个包含单个字段谓词的查询或使用 “MapId”。以下示例展示了一个带有扁平复合主键的类:

Example 8. Using a flat composite primary key
/*
 * Copyright 2020-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.cassandra.example;

import java.time.LocalDateTime;

import org.springframework.data.cassandra.core.cql.Ordering;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
import org.springframework.data.cassandra.core.mapping.Table;

// tag::class[]
@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
}
// end::class[]

Primary Key Class

主键类是一个复合主键类,映射到实体的多个字段或属性。它使用 “@PrimaryKeyClass” 进行注解,并应定义 “equals” 和 “hashCode” 方法。这些方法的值相等语义应与映射到主键的数据库类型的数据库相等一致。主键类可与存储库(作为 “Id” 类型)一起使用,也可以用于在一个复杂对象中表示实体的身份。以下示例展示了一个复合主键类:

Example 9. Composite primary key class
/*
 * Copyright 2020-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.cassandra.example;

import java.io.Serializable;
import java.time.LocalDateTime;

import org.springframework.data.cassandra.core.cql.Ordering;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;

// tag::class[]
@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
}
// end::class[]

以下示例展示如何使用复合主键:

Example 10. Using a composite primary key
@Table(value = "login_event")
public class LoginEvent {

  @PrimaryKey
  private LoginEventKey key;

  @Column("ip_address")
  private String ipAddress;

  // getters and setters omitted
}

Embedded Entity Support

嵌入式实体用于在你的 Java 领域模型中设计值对象,该模型的属性展平至表中。在以下示例中,你可以看到:User.name 使用 “@Embedded” 进行注解。这样做的结果是 UserName 的所有属性将折叠到 user 表中,该表包含 3 个列(user_idfirstnamelastname)。

嵌入式实体只能包含简单属性类型。无法将一个嵌入式实体嵌套到另一个嵌入式实体中。

然而,如果 firstnamelastname 列值在结果集中实际上为 null,则根据 “@Embedded” 的 onEmpty,整个属性 name 将被设置为 null,当所有嵌套属性为 null 时,null 为对象。与此行为相反,USE_EMPTY 尝试使用默认构造函数或接收来自结果集的可为 null 的参数值的构造函数来创建一个新实例。

Example 11. Sample Code of embedding objects
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 属性为`null`,如果`firstname`和`lastname`为`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)`快捷方式。

Mapping Annotation Overview

MappingCassandraConverter 可以使用元数据来驱动将对象映射到 Cassandra 表中的行。以下是对注解的概述:

  • @Id:应用于字段或属性级别,标示用于标识目的的属性。

  • @Table:应用于类级别,指示此类适用于映射到数据库。您可以指定对象存储的表名称。

  • @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&lt;@Frozen UserDefinedPersonType&gt;`。

  • @UserDefinedType:应用于类型级别,指定Cassandra用户定义的数据类型(UDT)。默认情况下,类型由声明派生。

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

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

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

映射元数据基础设施在独立的 spring-data-commons 项目中进行定义,该项目与技术和数据存储无关。

以下示例展示了一个更复杂的映射:

Example 12. Mapped Person class
/*
 * Copyright 2020-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.cassandra.example.mapping;

import java.io.Serializable;
import java.net.InetAddress;
import java.util.Map;
import java.util.Set;

import org.springframework.data.annotation.Transient;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
import org.springframework.data.cassandra.core.mapping.Table;

import com.datastax.oss.driver.api.core.data.UdtValue;

// tag::class[]
@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
}
// end::class[]

以下示例展示如何映射一个 UDT “Address”:

Example 13. Mapped User-Defined Type Address
/*
 * Copyright 2020-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.cassandra.example.mapping;

import java.util.List;
import java.util.Set;

import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.cassandra.core.mapping.UserDefinedType;

// tag::class[]
@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
}
// end::class[]

使用用户定义类型需要使用已通过映射上下文配置的 UserTypeResolver。请参阅 configuration chapter 以了解如何配置 UserTypeResolver

以下示例展示如何映射一个元组:

Example 14. Mapped Tuple
/*
 * Copyright 2020-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.cassandra.example.mapping;

import org.springframework.data.cassandra.core.mapping.CassandraType;
import org.springframework.data.cassandra.core.mapping.Element;
import org.springframework.data.cassandra.core.mapping.Tuple;

// tag::class[]
@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
}
// end::class[]

Index Creation

如果你想要在应用程序启动时创建二级索引,你可以使用 “@Indexed” 或 “@SASI” 为特定的实体属性添加注解。索引创建为标量类型、用户定义类型和集合类型创建简单二级索引。

你可以配置一个 SASI 索引来应用分析器,例如 StandardAnalyzerNonTokenizingAnalyzer(分别通过使用 “@StandardAnalyzed” 和 “@NonTokenizingAnalyzed”)。

映射类型在“条目”、“键”和“值”索引之间进行区分。索引创建从带注释的元素中派生索引类型。以下示例显示了创建索引的一些方法:

Example 15. Variants of map indexing
/*
 * Copyright 2020-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.cassandra.example.mapping;

import java.util.Map;

import org.springframework.data.annotation.Id;
import org.springframework.data.cassandra.core.mapping.Indexed;
import org.springframework.data.cassandra.core.mapping.SASI;
import org.springframework.data.cassandra.core.mapping.SASI.StandardAnalyzed;
import org.springframework.data.cassandra.core.mapping.Table;

// tag::class[]
@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;

  // …
}
// end::class[]

@Indexed 注释可以应用于嵌入实体的单个属性或与 @Embedded 注释同时使用,在此情况下,嵌入的所有属性都将被索引。

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