Mapping
MappingCassandraConverter
提供丰富的对象映射支持。MappingCassandraConverter
拥有丰富的元数据模型,该模型提供了将域对象映射到 CQL 表的功能的完整特性集。
Rich object mapping support is provided by the MappingCassandraConverter
. MappingCassandraConverter
has a rich metadata model that provides a complete feature set of functionality to map domain objects to CQL tables.
映射元数据模型通过使用域对象上的注释来填充。然而,该基础结构并不限制将注释用作唯一元数据源。通过遵循一系列约定,MappingCassandraConverter
还允许您将域对象映射到表,而无需提供任何其他元数据。
The mapping metadata model is populated by using annotations on your domain objects.
However, the infrastructure is not limited to using annotations as the only source of metadata.
The MappingCassandraConverter
also lets you map domain objects to tables without providing any additional metadata, by following a set of conventions.
在本章中,我们将描述 MappingCassandraConverter
的特性、如何使用约定将域对象映射到表,以及如何使用基于注释的映射元数据覆盖这些约定。
In this chapter, we describe the features of the MappingCassandraConverter
, how to use conventions for mapping domain objects to tables, and how to override those conventions with annotation-based mapping metadata.
Object Mapping Fundamentals
本节涵盖了 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不可变性的基础知识。请注意,此部分只适用于未使用基础数据存储的对象映射(如 JPA)的 Spring Data 模块。还要务必查阅特定于存储的部分,了解特定于存储的对象映射,如索引、自定义列或字段名称等。
This section covers the fundamentals of Spring Data object mapping, object creation, field and property access, mutability and immutability. Note, that this section only applies to Spring Data modules that do not use the object mapping of the underlying data store (like JPA). Also be sure to consult the store-specific sections for store-specific object mapping, like indexes, customizing column or field names or the like.
Spring Data 对象映射的核心职责是创建域对象实例并将存储本地数据结构映射到这些实例上。这意味着我们需要两个基本步骤:
Core responsibility of the Spring Data object mapping is to create instances of domain objects and map the store-native data structures onto those. This means we need two fundamental steps:
-
Instance creation by using one of the constructors exposed.
-
Instance population to materialize all exposed properties.
Object creation
Spring Data 自动尝试检测一个持久实体的构造函数,以便用于实现该类型的对象。解决算法的工作方式如下:
Spring Data automatically tries to detect a persistent entity’s constructor to be used to materialize objects of that type. The resolution algorithm works as follows:
-
If there is a single static factory method annotated with
@PersistenceCreator
then it is used. -
If there is a single constructor, it is used.
-
If there are multiple constructors and exactly one is annotated with
@PersistenceCreator
, it is used. -
If the type is a Java
Record
the canonical constructor is used. -
If there’s a no-argument constructor, it is used. Other constructors will be ignored.
值解析假定构造函数/工厂方法的参数名称与实体的属性名称匹配,即解析将执行得好像要填充该属性一样,包括映射中的所有定制(不同的数据存储列或字段名称等)。这也要求在类文件或构造函数上有 @ConstructorProperties
注释中提供参数名称信息。
The value resolution assumes constructor/factory method argument names to match the property names of the entity, i.e. the resolution will be performed as if the property was to be populated, including all customizations in mapping (different datastore column or field name etc.).
This also requires either parameter names information available in the class file or an @ConstructorProperties
annotation being present on the constructor.
可以通过使用 Spring Framework 的 @Value
值注释来使用特定于存储的 SpEL 表达式来定制值解析。请查阅有关具体存储的映射的部分以获取更多详细信息。
The value resolution can be customized by using Spring Framework’s @Value
value annotation using a store-specific SpEL expression.
Please consult the section on store specific mappings for further details.
为了避免反射时间开销,Spring Data 对象创建在默认情况下使用在运行时生成的工厂类,该类将直接调用领域类的构造函数。对于以下示例类型:
To avoid the overhead of reflection, Spring Data object creation uses a factory class generated at runtime by default, which will call the domain classes constructor directly. I.e. for this example type:
class Person {
Person(String firstname, String lastname) { … }
}
我们将在运行时为这个类创建一个语义上等价的工厂类:
we will create a factory class semantically equivalent to this one at runtime:
class PersonObjectInstantiator implements ObjectInstantiator {
Object newInstance(Object... args) {
return new Person((String) args[0], (String) args[1]);
}
}
这给了我们超过反射 10% 的迂回式性能提升。为了使领域类适用于这种优化,它需要遵守一组约束:
This gives us a roundabout 10% performance boost over reflection. For the domain class to be eligible for such optimization, it needs to adhere to a set of constraints:
-
it must not be a private class
-
it must not be a non-static inner class
-
it must not be a CGLib proxy class
-
the constructor to be used by Spring Data must not be private
如果符合上述任何一个标准,Spring Data 将回退到通过反射进行实体实例化。
If any of these criteria match, Spring Data will fall back to entity instantiation via reflection.
Property population
在创建了该实体的一个实例之后,Spring Data 填充该类的所有剩余的持久属性。除非已通过实体的构造函数填充(即通过其构造函数参数列表消耗),否则将首先填充标识符属性,以允许解析循环对象引用。在此之后,所有尚未被构造函数填充的非瞬态属性都会在实体实例上设置。为此,我们使用以下算法:
Once an instance of the entity has been created, Spring Data populates all remaining persistent properties of that class. Unless already populated by the entity’s constructor (i.e. consumed through its constructor argument list), the identifier property will be populated first to allow the resolution of cyclic object references. After that, all non-transient properties that have not already been populated by the constructor are set on the entity instance. For that we use the following algorithm:
-
If the property is immutable but exposes a
with…
method (see below), we use thewith…
method to create a new entity instance with the new property value. -
If property access (i.e. access through getters and setters) is defined, we’re invoking the setter method.
-
If the property is mutable we set the field directly.
-
If the property is immutable we’re using the constructor to be used by persistence operations (see mapping.object-creation) to create a copy of the instance.
-
By default, we set the field value directly.
类似于我们在对象构造中的优化,映射.object-creation.details,我们还使用 Spring Data 运行时生成的访问器类与实体实例进行交互。
Similarly to our mapping.object-creation.details we also use Spring Data runtime generated accessor classes to interact with the entity instance.
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’s hold a mutable instance of the underlying object. This is, to enable mutations of otherwise immutable properties. |
2 | By default, Spring Data uses field-access to read and write property values. As per visibility rules of private fields, MethodHandles are used to interact with fields. |
3 | The class exposes a withId(…) method that’s used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated. Calling withId(…) creates a new Person object. All subsequent mutations will take place in the new instance leaving the previous untouched. |
4 | Using property-access allows direct method invocations without using MethodHandles . |
这给了我们超过反射 25% 的迂回式性能提升。为了使领域类适用于这种优化,它需要遵守一组约束:
This gives us a roundabout 25% performance boost over reflection. For the domain class to be eligible for such optimization, it needs to adhere to a set of constraints:
-
Types must not reside in the default or under the
java
package. -
Types and their constructors must be
public
-
Types that are inner classes must be
static
. -
The used Java Runtime must allow for declaring classes in the originating
ClassLoader
. Java 9 and newer impose certain limitations.
在默认情况下,Spring Data 尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的属性访问器。
By default, Spring Data attempts to use generated property accessors and falls back to reflection-based ones if a limitation is detected.
让我们看看以下实体:
Let’s have a look at the following 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 | The identifier property is final but set to null in the constructor.
The class exposes a withId(…) method that’s used to set the identifier, e.g. when an instance is inserted into the datastore and an identifier has been generated.
The original Person instance stays unchanged as a new one is created.
The same pattern is usually applied for other properties that are store managed but might have to be changed for persistence operations.
The wither method is optional as the persistence constructor (see 6) is effectively a copy constructor and setting the property will be translated into creating a fresh instance with the new identifier value applied. |
2 | The firstname and lastname properties are ordinary immutable properties potentially exposed through getters. |
3 | The age property is an immutable but derived one from the birthday property.
With the design shown, the database value will trump the defaulting as Spring Data uses the only declared constructor.
Even if the intent is that the calculation should be preferred, it’s important that this constructor also takes age as parameter (to potentially ignore it) as otherwise the property population step will attempt to set the age field and fail due to it being immutable and no with… method being present. |
4 | The comment property is mutable and is populated by setting its field directly. |
5 | The remarks property is mutable and is populated by invoking the setter method. |
6 | The class exposes a factory method and a constructor for object creation.
The core idea here is to use factory methods instead of additional constructors to avoid the need for constructor disambiguation through @PersistenceCreator .
Instead, defaulting of properties is handled within the factory method.
If you want Spring Data to use the factory method for object instantiation, annotate it with @PersistenceCreator . |
General recommendations
-
Try to stick to immutable objects — Immutable objects are straightforward to create as materializing an object is then a matter of calling its constructor only. Also, this avoids your domain objects to be littered with setter methods that allow client code to manipulate the objects state. If you need those, prefer to make them package protected so that they can only be invoked by a limited amount of co-located types. Constructor-only materialization is up to 30% faster than properties population.
-
Provide an all-args constructor — Even if you cannot or don’t want to model your entities as immutable values, there’s still value in providing a constructor that takes all properties of the entity as arguments, including the mutable ones, as this allows the object mapping to skip the property population for optimal performance.
-
Use factory methods instead of overloaded constructors to avoid `@PersistenceCreator` — With an all-argument constructor needed for optimal performance, we usually want to expose more application use case specific constructors that omit things like auto-generated identifiers etc. It’s an established pattern to rather use static factory methods to expose these variants of the all-args constructor.
-
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 — As persistence operations usually require a constructor taking all arguments, their declaration becomes a tedious repetition of boilerplate parameter to field assignments that can best be avoided by using Lombok’s
@AllArgsConstructor
.
Overriding Properties
Java 允许对领域类进行灵活的设计,其中子类可以定义在超类中已经使用相同名称声明的属性。考虑以下示例:
Java’s allows a flexible design of domain classes where a subclass could define a property that is already declared with the same name in its superclass. Consider the following example:
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 将跳过超类型属性。也就是说,被重写的属性的类型必须可以分配给其超类型属性类型才能被注册为覆盖项,否则超类型属性被视为瞬态。我们通常建议使用不同的属性名称。
Both classes define a field
using assignable types. SubType
however shadows SuperType.field
.
Depending on the class design, using the constructor could be the only default approach to set SuperType.field
.
Alternatively, calling super.setField(…)
in the setter could set the field
in SuperType
.
All these mechanisms create conflicts to some degree because the properties share the same name yet might represent two distinct values.
Spring Data skips super-type properties if types are not assignable.
That is, the type of the overridden property must be assignable to its super-type property type to be registered as override, otherwise the super-type property is considered transient.
We generally recommend using distinct property names.
Spring Data 模块普遍支持包含不同值的重写属性。从编程模型的角度来看,有几件事需要考虑:
Spring Data modules generally support overridden properties holding different values. From a programming model perspective there are a few things to consider:
-
Which property should be persisted (default to all declared properties)? You can exclude properties by annotating these with
@Transient
. -
How to represent properties in your data store? Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name.
-
Using
@AccessType(PROPERTY)
cannot be used as the super-property cannot be generally set without making any further assumptions of the setter implementation.
Kotlin support
Spring Data 适应 Kotlin 的特殊性,允许创建和更改对象。
Spring Data adapts specifics of Kotlin to allow object creation and mutation.
Kotlin object creation
支持实例化 Kotlin 类,所有类在默认情况下都是不可变的,需要显式属性声明才能定义可变属性。
Kotlin classes are supported to be instantiated, all classes are immutable by default and require explicit property declarations to define mutable properties.
Spring Data 自动尝试检测一个持久实体的构造函数,以便用于实现该类型的对象。解决算法的工作方式如下:
Spring Data automatically tries to detect a persistent entity’s constructor to be used to materialize objects of that type. The resolution algorithm works as follows:
-
If there is a constructor that is annotated with
@PersistenceCreator
, it is used. -
If the type is a mapping.kotlin the primary constructor is used.
-
If there is a single static factory method annotated with
@PersistenceCreator
then it is used. -
If there is a single constructor, it is used.
-
If there are multiple constructors and exactly one is annotated with
@PersistenceCreator
, it is used. -
If the type is a Java
Record
the canonical constructor is used. -
If there’s a no-argument constructor, it is used. Other constructors will be ignored.
考虑以下 data
类 Person
:
Consider the following data
class Person
:
data class Person(val id: String, val name: String)
上述类编译为具有显式构造函数的典型类。我们可以通过添加另一个构造函数并用 @PersistenceCreator
注释它来定制此类,以指示构造函数偏好:
The class above compiles to a typical class with an explicit constructor.We can customize this class by adding another constructor and annotate it with @PersistenceCreator
to indicate a constructor preference:
data class Person(var id: String, val name: String) {
@PersistenceCreator
constructor(id: String) : this(id, "unknown")
}
Kotlin 支持参数可选择性,允许在不提供参数时使用默认值。当 Spring Data 检测到带有参数默认值的构造函数时,它会让这些参数保持缺失状态(或仅返回 null
),以便 Kotlin 可以应用参数默认值。考虑以下对 name
应用参数默认值的类
Kotlin supports parameter optionality by allowing default values to be used if a parameter is not provided.
When Spring Data detects a constructor with parameter defaulting, then it leaves these parameters absent if the data store does not provide a value (or simply returns null
) so Kotlin can apply parameter defaulting.Consider the following class that applies parameter defaulting for name
data class Person(var id: String, val name: String = "unknown")
每次当 name
参数既不属于结果的一部分或其值为 null
,则 name
将默认为 unknown
。
Every time the name
parameter is either not part of the result or its value is null
, then the name
defaults to unknown
.
Property population of Kotlin data classes
在 Kotlin 中,所有类默认情况下是不可变的,需要显式声明属性来定义可变属性。考虑下面的 data
类 Person
:
In Kotlin, all classes are immutable by default and require explicit property declarations to define mutable properties.
Consider the following data
class Person
:
data class Person(val id: String, val name: String)
这个类实际上是不可变的。它允许创建新的实例,因为 Kotlin 生成了一个 copy(…)
方法,该方法创建新的对象实例,复制所有属性值(从现有对象)并将作为参数提供给该方法的属性值应用进来。
This class is effectively immutable.
It allows creating new instances as Kotlin generates a copy(…)
method that creates new object instances copying all property values from the existing object and applying property values provided as arguments to the method.
Kotlin Overriding Properties
Kotlin 允许声明 `` 来更改子类中的属性。
Kotlin allows declaring property overrides to alter properties in subclasses.
open class SuperType(open var field: Int)
class SubType(override var field: Int = 1) :
SuperType(field) {
}
这种安排使两个名称为 field
的属性得以呈现。Kotlin 中为每个类中每个属性生成了属性访问器(getter 和 setter)。实际上,该代码如下所示:
Such an arrangement renders two properties with the name field
.
Kotlin generates property accessors (getters and setters) for each property in each class.
Effectively, the code looks like as follows:
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
是可能的,但是不属于受支持的约定。属性覆盖在某种程度上创建了冲突,因为这些属性共享相同名称,但可能表示两个不同的值。我们通常建议使用不同的属性名称。
Getters and setters on SubType
set only SubType.field
and not SuperType.field
.
In such an arrangement, using the constructor is the only default approach to set SuperType.field
.
Adding a method to SubType
to set SuperType.field
via this.SuperType.field = …
is possible but falls outside of supported conventions.
Property overrides create conflicts to some degree because the properties share the same name yet might represent two distinct values.
We generally recommend using distinct property names.
Spring Data 模块普遍支持包含不同值的重写属性。从编程模型的角度来看,有几件事需要考虑:
Spring Data modules generally support overridden properties holding different values. From a programming model perspective there are a few things to consider:
-
Which property should be persisted (default to all declared properties)? You can exclude properties by annotating these with
@Transient
. -
How to represent properties in your data store? Using the same field/column name for different values typically leads to corrupt data so you should annotate least one of the properties using an explicit field/column name.
-
Using
@AccessType(PROPERTY)
cannot be used as the super-property cannot be set.
Kotlin Value Classes
Kotlin 值类被设计用于更具表现力的领域模型来使基础概念明确化。Spring Data 可以读取和写入使用值类定义属性的类型。
Kotlin Value Classes are designed for a more expressive domain model to make underlying concepts explicit. Spring Data can read and write types that define properties using Value Classes.
考虑以下领域模型:
Consider the following domain model:
@JvmInline
value class EmailAddress(val theAddress: String) 1
data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) 2
1 | A simple value class with a non-nullable value type. |
2 | Data class defining a property using the EmailAddress value class. |
在已编译类中,使用非基本值类型而非空属性会展平为该值类型。可空基本值类型或可空值中值类型将用包装器类型表示,并影响在数据库中如何表示值类型。 |
Non-nullable properties using non-primitive value types are flattened in the compiled class to the value type. Nullable primitive value types or nullable value-in-value types are represented with their wrapper type and that affects how value types are represented in the database. |
Data Mapping and Type Conversion
本节说明了如何将类型映射到 Apache Cassandra 表示形式,以及如何从 Apache Cassandra 表示形式映射类型。
This section explains how types are mapped to and from an Apache Cassandra representation.
Apache Cassandra 的 Spring Data 支持 Apache Cassandra 提供的多种类型。除了这些类型之外,Apache Cassandra 的 Spring Data 还提供了一组内置转换器来映射其他类型。您可以提供自己的自定义转换器来调整类型转换。有关更多详细信息,请参见 “Overriding Default Mapping with Custom Converters”。下表将 Spring Data 类型映射到 Cassandra 类型:
Spring Data for Apache Cassandra supports several types that are provided by Apache Cassandra. In addition to these types, Spring Data for Apache Cassandra provides a set of built-in converters to map additional types. You can provide your own custom converters to adjust type conversion. See “Overriding Default Mapping with Custom Converters” for further details. The following table maps Spring Data types to Cassandra types:
Type | Cassandra types |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
user type |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
每种受支持的类型映射到一个默认 Cassandra data type。通过使用 @CassandraType
,Java 类型可以映射到其他 Cassandra 类型,如下面的示例所示:
Each supported type maps to a default
Cassandra data type.
Java types can be mapped to other Cassandra types by using @CassandraType
, as the following example shows:
@Table
public class EnumToOrdinalMapping {
@PrimaryKey String id;
@CassandraType(type = Name.INT) Condition asOrdinal;
}
public enum Condition {
NEW, USED
}
Convention-based Mapping
当不提供其他映射元数据时,MappingCassandraConverter
使用一些约定将域对象映射到 CQL 表。这些约定是:
MappingCassandraConverter
uses a few conventions for mapping domain objects to CQL tables when no additional mapping metadata is provided.
The conventions are:
-
The simple (short) Java class name is mapped to the table name by being changed to lower case. For example,
com.bigbank.SavingsAccount
maps to a table namedsavingsaccount
. -
The converter uses any registered Spring
Converter
instances to override the default mapping of object properties to tables columns. -
The properties of an object are used to convert to and from columns in the table.
您可以通过在 CassandraMappingContext
上配置 NamingStrategy
来调整约定。命名策略对象实现了根据实体类和实际属性派生表、列或用户定义类型的约定。
You can adjust conventions by configuring a NamingStrategy
on CassandraMappingContext
.
Naming strategy objects implement the convention by which a table, column or user-defined type is derived from an entity class and from an actual property.
以下示例演示如何配置 NamingStrategy
:
The following example shows how to configure a NamingStrategy
:
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
实例,以告诉它在启动时在类路径的何处扫描域类,以提取元数据和构建索引。
Unless explicitly configured, an instance of MappingCassandraConverter
is created by default when creating a CassandraTemplate
.
You can create your own instance of the MappingCassandraConverter
to tell it where to scan the classpath at startup for your domain classes to extract metadata and construct indexes.
此外,通过创建自己的实例,您可以注册 Spring Converter
实例,以便用于将特定类映射到数据库中并再从中映射出来。以下示例配置类设置了 Cassandra 映射支持:
Also, by creating your own instance, you can register Spring Converter
instances to use for mapping specific classes to and from the database.
The following example configuration class sets up 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
注释的类。
AbstractCassandraConfiguration
requires you to implement methods that define a keyspace.
AbstractCassandraConfiguration
also has a method named getEntityBasePackages(…)
.
You can override it to tell the converter where to scan for classes annotated with the @Table
annotation.
可以通过覆盖 customConversions
方法向 MappingCassandraConverter
添加其他转换器。
You can add additional converters to the MappingCassandraConverter
by overriding the customConversions
method.
|
|
Metadata-based Mapping
要充分利用 Spring Data for Apache Cassandra 支持中的对象映射功能,您应该使用 @Table
注释注释您的映射域对象。这样操作可以让类路径扫描器找到并预处理域对象,以提取必要的元数据。只有带注释的实体用于执行模式操作。在最坏的情况下,SchemaAction.RECREATE_DROP_UNUSED
操作会删除您的表,并且您会丢失数据。以下示例显示了一个简单的域对象:
To take full advantage of the object mapping functionality inside the Spring Data for Apache Cassandra support, you should annotate your mapped domain objects with the @Table
annotation.
Doing so lets the classpath scanner find and pre-process your domain objects to extract the necessary metadata.
Only annotated entities are used to perform schema actions.
In the worst case, a SchemaAction.RECREATE_DROP_UNUSED
operation drops your tables and you lose your data.
The following example shows a simple 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 主键的属性。复合主键可能需要略有不同的数据模型。
The @Id
annotation tells the mapper which property you want to use for the Cassandra primary key.
Composite primary keys can require a slightly different data model.
Working with Primary Keys
Cassandra 要求 CQL 表至少有一个分区键字段。此外,一个表还可以声明一个或多个聚集键字段。当您的 CQL 表具有复合主键时,您必须创建一个 @PrimaryKeyClass
来定义复合主键的结构。在此上下文中,“`复合主键`" 意味着一个或多个分区列,这些分区列可以与一个或多个聚集列相结合(可选)。
Cassandra requires at least one partition key field for a CQL table.
A table can additionally declare one or more clustering key fields.
When your CQL table has a composite primary key, you must create a @PrimaryKeyClass
to define the structure of the composite primary key.
In this context, “composite primary key” means one or more partition columns optionally combined with one or more clustering columns.
主键可以使用任何单一简单 Cassandra 类型或映射的用户定义类型。不支持集合类型的关键字。
Primary keys can make use of any singular simple Cassandra type or mapped user-defined Type. Collection-typed primary keys are not supported.
Simple Primary Keys
简单主键由一个实体类中的一个分区键字段组成。因为它只有一个字段,所以我们可以安全地认为它是一个分区键。以下列表显示了在 Cassandra 中定义带有 user_id
主键的 CQL 表:
A simple primary key consists of one partition key field within an entity class.
Since it is one field only, we safely can assume it is a partition key.
The following listing shows a CQL table defined in Cassandra with a primary key of user_id
:
CREATE TABLE user (
user_id text,
firstname text,
lastname text,
PRIMARY KEY (user_id))
;
以下示例展示了一个 Java 类,在使用注解的情况下,它与前面列表中定义的 Cassandra 相对应:
The following example shows a Java class annotated such that it corresponds to the Cassandra defined in the previous listing:
@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
复合主键(或复合键)由多个主键字段组成。也就是说,一个复合主键可以由多个分区键、一个分区键加一个集群键或一组主键字段组成。
Composite primary keys (or compound keys) consist of more than one primary key field. That said, a composite primary key can consist of multiple partition keys, a partition key and a clustering key, or a multitude of primary key fields.
使用适用于 Apache Cassandra 的 Spring Data 可以用两种方式表示复合键:
Composite keys can be represented in two ways with Spring Data for Apache Cassandra:
-
Embedded in an entity.
-
By using
@PrimaryKeyClass
.
复合键最简单的形式是带有分区键和集群键的键。
The simplest form of a composite key is a key with one partition key and one clustering key.
以下示例展示一个 CQL 语句以表示表及其复合键:
The following example shows a CQL statement to represent the table and its composite 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”。以下示例展示了一个带有扁平复合主键的类:
Flat composite primary keys are embedded inside the entity as flat fields.
Primary key fields are annotated with
@PrimaryKeyColumn
.
Selection requires either a query to contain predicates for the individual fields or the use of MapId
.
The following example shows a class with 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” 类型)一起使用,也可以用于在一个复杂对象中表示实体的身份。以下示例展示了一个复合主键类:
A primary key class is a composite primary key class that is mapped to multiple fields or properties of the entity.
It is annotated with @PrimaryKeyClass
and should define equals
and hashCode
methods.
The semantics of value equality for these methods should be consistent with the database equality for the database types to which the key is mapped.
Primary key classes can be used with repositories (as the Id
type) and to represent an entity’s identity in a single complex object.
The following example shows a 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[]
以下示例展示如何使用复合主键:
The following example shows how to use 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_id
、firstname
、lastname
)。
Embedded entities are used to design value objects in your Java domain model whose properties are flattened out into the table.
In the following example you see, that User.name
is annotated with @Embedded
.
The consequence of this is that all properties of UserName
are folded into the user
table which consists of 3 columns (user_id
, firstname
, lastname
).
嵌入式实体只能包含简单属性类型。无法将一个嵌入式实体嵌套到另一个嵌入式实体中。 Embedded entities may only contain simple property types. It is not possible to nest an embedded entity into another embedded one. |
然而,如果 firstname
和 lastname
列值在结果集中实际上为 null
,则根据 “@Embedded” 的 onEmpty
,整个属性 name
将被设置为 null
,当所有嵌套属性为 null
时,null
为对象。与此行为相反,USE_EMPTY
尝试使用默认构造函数或接收来自结果集的可为 null 的参数值的构造函数来创建一个新实例。
However, if the firstname
and lastname
column values are actually null
within the result set, the entire property name
will be set to null
according to the onEmpty
of @Embedded
, which null`s objects when all nested properties are `null
.
Opposite to this behavior USE_EMPTY
tries to create a new instance using either a default constructor or one that accepts nullable parameter values from the result set.
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 | Property is null if firstname and lastname are null .
Use onEmpty=USE_EMPTY to instantiate UserName with a potential null value for its properties. |
你可以通过使用 “@Embedded” 注解的可选 prefix
元素,在实体中多次嵌入一个值对象。这个元素表示一个前缀,并添加到嵌入对象中每个列名前。请注意,如果多个属性渲染到相同的列名,则属性将互相覆盖。
You can embed a value object multiple times in an entity by using the optional prefix
element of the @Embedded
annotation.
This element represents a prefix and is prepended to each column name in the embedded object.
Note that properties will overwrite each other if multiple properties render to the same column name.
利用快捷方式 Make use of the shortcuts
|
Mapping Annotation Overview
MappingCassandraConverter
可以使用元数据来驱动将对象映射到 Cassandra 表中的行。以下是对注解的概述:
The MappingCassandraConverter
can use metadata to drive the mapping of objects to rows in a Cassandra table.
An overview of the annotations follows:
-
@Id
: Applied at the field or property level to mark the property used for identity purposes. -
@Table
: Applied at the class level to indicate that this class is a candidate for mapping to the database. You can specify the name of the table where the object is stored. -
@PrimaryKey
: Similar to@Id
but lets you specify the column name. -
@PrimaryKeyColumn
: Cassandra-specific annotation for primary key columns that lets you specify primary key column attributes, such as for clustered or partitioned. Can be used on single and multiple attributes to indicate either a single or a composite (compound) primary key. If used on a property within the entity, make sure to apply the@Id
annotation as well. -
@PrimaryKeyClass
: Applied at the class level to indicate that this class is a compound primary key class. Must be referenced with@PrimaryKey
in the entity class. -
@Transient
: By default, all private fields are mapped to the row. This annotation excludes the field where it is applied from being stored in the database. Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument. -
@PersistenceConstructor
: Marks a given constructor — even a package protected one — to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved row. -
@Value
: This annotation is part of the Spring Framework . Within the mapping framework it can be applied to constructor arguments. This lets you use a Spring Expression Language statement to transform a key’s value retrieved in the database before it is used to construct a domain object. In order to reference a property of a givenRow
/UdtValue
/TupleValue
one has to use expressions like:@Value("#root.getString(0)")
whereroot
refers to the root of the given document. -
@ReadOnlyProperty
: Applies at the field level to mark a property as read-only. Entity-bound insert and update statements do not include this property. -
@Column
: Applied at the field level. Describes the column name as it is represented in the Cassandra table, thus letting the name differ from the field name of the class. Can be used on constructor arguments to customize the column name during constructor creation. -
@Embedded
: Applied at the field level. Enables embedded object usage for types mapped to a table or a user-defined type. Properties of the embedded object are flattened into the structure of its parent. -
@Indexed
: Applied at the field level. Describes the index to be created at session initialization. -
@SASI
: Applied at the field level. Allows SASI index creation during session initialization. -
@CassandraType
: Applied at the field level to specify a Cassandra data type. Types are derived from the property declaration by default. -
@Frozen
: Applied at the field level to class-types and parametrized types. Declares a frozen UDT column or frozen collection likeList<@Frozen UserDefinedPersonType>
. -
@UserDefinedType
: Applied at the type level to specify a Cassandra User-defined Data Type (UDT). Types are derived from the declaration by default. -
@Tuple
: Applied at the type level to use a type as a mapped tuple. -
@Element
: Applied at the field level to specify element or field ordinals within a mapped tuple. Types are derived from the property declaration by default. Can be used on constructor arguments to customize tuple element ordinals during constructor creation. -
@Version
: Applied at field level is used for optimistic locking and checked for modification on save operations. The initial value iszero
which is bumped automatically on every update.
映射元数据基础设施在独立的 spring-data-commons 项目中进行定义,该项目与技术和数据存储无关。
The mapping metadata infrastructure is defined in the separate, spring-data-commons project that is both technology- and data store-agnostic.
以下示例展示了一个更复杂的映射:
The following example shows a more complex mapping:
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”:
The following example shows how to map a UDT Address
:
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[]
使用用户定义类型需要使用已通过映射上下文配置的 |
Working with User-Defined Types requires a |
以下示例展示如何映射一个元组:
The following example shows how map a 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” 为特定的实体属性添加注解。索引创建为标量类型、用户定义类型和集合类型创建简单二级索引。
You can annotate particular entity properties with @Indexed
or @SASI
if you wish to create secondary indexes on application startup.
Index creation creates simple secondary indexes for scalar types, user-defined types, and collection types.
你可以配置一个 SASI 索引来应用分析器,例如 StandardAnalyzer
或 NonTokenizingAnalyzer
(分别通过使用 “@StandardAnalyzed” 和 “@NonTokenizingAnalyzed”)。
You can configure a SASI Index to apply an analyzer, such as StandardAnalyzer
or NonTokenizingAnalyzer
(by using
@StandardAnalyzed
and @NonTokenizingAnalyzed
, respectively).
映射类型在“条目”、“键”和“值”索引之间进行区分。索引创建从带注释的元素中派生索引类型。以下示例显示了创建索引的一些方法:
Map types distinguish between ENTRY
, KEYS
, and VALUES
indexes.
Index creation derives the index type from the annotated element.
The following example shows a number of ways to create an index:
/*
* 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[]
The |
在会话初始化时创建索引可能会严重影响应用程序启动时的性能。
Index creation on session initialization may have a severe performance impact on application startup.