天天看点

Spring Data for Apache Cassandra - Reference Documentation - 14.MappingSpring Data for Apache Cassandra - Reference Documentation

Spring Data for Apache Cassandra - Reference Documentation

14. Mapping

        MappingCassandraConverter提供了丰富的对象映射支持。MappingCassandraConverter有一个丰富的元数据模型,它提供了一组完整的功能,可以将域对象映射到CQL表。

        元数据映射模型(Mapping metadata model)通过域对象上的注解来填充的(populated)。然而,不仅限于将元数据作为基础设置的源。MappingCassandraConverter还可以通过遵循一组约定将域对象映射到表,而无需提供任何额外的元数据。

        在本章中,我们将介绍MappingCassandraConverter的特性,如何使用将域对象映射到表的约定,以及如何使用基于注解的映射元数据覆盖这些约定。

14.1. 对象映射基本原则

本节介绍Spring Data对象映射、对象创建、字段和属性访问、可变和不可变的基础知识。注意,本节只适用于不使用底层数据存储(如JPA)的对象映射的Spring Data模块。另外,请务必参考特定于存储的部分,以获取特定于存储的对象映射,如索引、自定义列或字段名等。

Spring Data对象映射的核心职责是创建域对象的实例,并将存储本机数据结构映射到这些实例上。这意味着我们需要两个基本步骤:

  1. Instance creation by using one of the constructors exposed. 使用公开的构造函数之一创建实例。
  2. Instance population to materialize all exposed properties. 实例填充以具体化所有公开的属性。

14.1.1. 对象创建

Spring Data自动尝试检测一个持久实体的构造函数,该构造函数将用于具体化该类型的对象。解析逻辑的工作原理如下:

  1. If there’s a no-argument constructor, it will be used. Other constructors will be ignored. 如果有无参构造函数,则使用无参构造函数。其他构造函数将被忽略。
  2. If there’s a single constructor taking arguments, it will be used. 如果只有一个有参构造函数,则使用这个构造函数。
  3. If there are multiple constructors taking arguments, the one to be used by Spring Data will have to be annotated with 

    @PersistenceConstructor

    . 如果有多个有参构造函数,则使用带有@PersistenceConstructor注解的构造函数。

值解析假定构造函数参数名与实体的属性名匹配,也就是说,执行解析时就像要填充属性一样,包括映射中的所有自定义项(不同的数据存储列或字段名等)。这还需要类文件中可用的参数名称信息,或者构造函数上存在@ConstructorProperties注释。

通过使用Spring框架的@Value 值注释,可以使用特定于存储的SpEL表达式来定制值解析。有关详细信息,请参阅有关特定于存储的映射的部分。

                                               Object creation internals

为了避免反射的开销,Spring Data对象创建使用默认情况下在运行时生成的工厂类,它将直接调用域类构造函数。 I.e. for this example type:

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%。为了使域类有资格进行此类优化,它需要遵守一组约束条件:

  • it must not be a private class 不能是私有类
  • it must not be a non-static inner class 不能是非静态内部类
  • it must not be a CGLib proxy class 不能是CGLib代理类
  • the constructor to be used by Spring Data must not be private 被Spring Data使用的构造函数不能是私有的

如果满足以上条件中的任何一个,Spring Data将通过反射返回实体实例化。

14.1.2. 属性填充

一旦创建了实体的实例,Spring Data将填充该类的所有剩余持久化属性。除非已经由实体的构造函数填充(即通过其构造函数参数列表使用),否则标识符(identifier, @Id注解的字段)属性将首先填充以允许解析循环对象引用。然后,在实体实例上设置所有尚未由构造函数填充的非瞬态(non-transient)属性。为此,我们使用以下逻辑:

  1. If the property is immutable but exposes a 

    with…

     method (see below), we use the 

    with…

     method to create a new entity instance with the new property value. 如果属性是不可变的,但是存在公开的with...方法,则使用with...方法来创建一个新的对象实例来新建属性值
  2. If property access (i.e. access through getters and setters) is defined, we’re invoking the setter method. 属性有定义入口方法,则调用setter方法
  3. If the property is mutable we set the field directly. 如果属性是可变的,直接赋值。
  4. If the property is immutable we’re using the constructor to be used by persistence operations (see Object creation) to create a copy of the instance. 如果属性是不可变的,则使用被持久化的构造函数来创建实例副本
  5. By default, we set the field value directly. 默认,直接赋值

                                          Property population internals

与对象构造中的优化类似,我们还使用Spring Data运行时生成的访问器类(accessor)与实体实例进行交互。

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;
  }
}
           

Example 97. 生成的属性访问器

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. PropertyAccessor持有基础对象的可变实例。这是为了使原本不可变的属性变为可变
  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. 默认情况下,Spring Data使用字段访问来读写属性值。根据私有字段的可见性规则,MethodHandles用于与字段交互。
  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. 该类公开了一个用于设置标识符的withId(...)方法,例如,当一个实例插入到数据存储中并且生成了一个标识符时。调用withId(...)将创建一个新的Person对象。所有后续的改变都将发生在新的实例中,而不影响前一个实例。
  4. Using property-access allows direct method invocations without using

    MethodHandles

    . 使用属性访问允许不使用MethodHandles直接调用方法。

这使我们的性能比反射提高了25%。为了使域类有资格进行此类优化,它需要遵守一组约束条件:

  • Types must not reside in the default or under the 

    java

     package. 字段类型必须在java包下或不能位于默认值中。
  • Types and their constructors must be 

    public

    . 字段类型和构造函数必须是公开的(public)的。
  • Types that are inner classes must be 

    static

    . 字段类型是内部类的必须是静态的(static)。
  • The used Java Runtime must allow for declaring classes in the originating 

    ClassLoader

    . Java 9 and newer impose certain limitations. 使用的Java Runtime必须允许在原始类加载器中声明类。Java9和更新版本会有一些限制。

默认情况下,Spring Data尝试使用生成的属性访问器,如果检测到限制,则返回到基于反射的访问器。

我们来看看下面这个实体:

Example 98. 实体案例

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.

identifier属性是final,但在构造函数中设置为null。这个类公开了一个用于设置identifier的withId(...)方法,例如,当一个实例插入到数据存储中并且生成了一个identifier时。在创建新实例时,原始Person实例保持不变。相同的模式通常应用于存储管理的其他属性,但可能必须为持久性操作更改这些属性。with方法是可选的,因为持久性构造函数(参见6)实际上是一个复制的构造函数,设置属性将转换为使用新的identifier值创建新的实例。

2 The

firstname

and

lastname

properties are ordinary immutable properties potentially exposed through getters.

firstname和lastname属性是普通的不可变属性,可能通过getter公开。

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.

age属性是不可变的,但从birthday属性派生而来。设计中显示,数据库值将胜过默认值,因为Spring Data只使用声明的构造函数。即使目的是优先考虑计算,但重要的是,此构造函数也将age作为参数(可能忽略它),否则属性填充步骤将尝试设置age字段,并且由于它是不可变的且不存在with…方法而失败。

4 The

comment

property is mutable is populated by setting its field directly.

comment属性是可变的,通过直接设置其字段来填充。

5 The

remarks

properties are mutable and populated by setting the

comment

field directly or by invoking the setter method for

remarks属性是可变的,通过直接设置comment字段或调用setter方法(来填充)

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

@PersistenceConstructor

. Instead, defaulting of properties is handled within the factory method.

该类公开用于对象创建的工厂方法和构造函数。这里的核心思想是使用工厂方法而不是附加的构造函数,以避免需要通过@PersistenceConstructor消除构造函数的歧义。相反,属性的默认值是在工厂方法中处理的。

14.1.3. 一般性建议

  • 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.
  • 尽量坚持使用不可变的对象 - 不可变对象很容易创建,因为具体化对象只需要调用其构造函数。此外,这可以避免您的域对象被setter方法搞乱(littered),这些方法允许客户端代码操作对象状态。如果需要它们,最好将它们设置为包保护,这样它们只能由有限数量的同位置类型调用。仅构造函数的物化比属性填充快30%。
  • 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 

    @PersistenceConstructor

     — 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.
  • 使用工厂方法而不是重载构造函数来避免@PersistenceConstructor - - 与最佳性能所需的全参数构造函数,我们通常希望公开更多特定于应用程序的构造函数,这些构造函数忽略了自动生成的标识符等内容。更倾向于使用静态工厂是一种既定模式方法来公开所有参数构造函数的这些变体。
  • 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 — 
  • 对于要生成的identifier,仍然使用final字段与全参数持久化构造函数(首选)或with…方法 -
  • 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

    .
  • 使用Lombok可以避免样板代码 - - 因为持久性操作通常需要一个构造函数接受所有参数,因此它们的声明变成了一个单调重复的样板参数到字段赋值,使用Lombok的@AllArgsConstructor可以最好地避免这种情况。

14.1.4. Kotlin支持

Spring数据对Kotlin的细节进行了调整,以允许对象的创建和可变。

Kotlin 对象创建

Kotlin类支持实例化,默认情况下所有类都是不可变的,需要显式的属性声明来定义可变的属性。考虑使用data class Person:

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

上面的类用显式构造函数编译成一个典型类。我们可以通过添加另一个构造函数自定义该类,并用@PersistenceConstructor对其进行注释,以指示构造函数的首选项:

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

    @PersistenceConstructor
    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中,默认情况下所有类都是不可变的,并且需要显式的属性声明来定义可变的属性。考虑以下data class Person:

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

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

14.2. 数据映射和类型转换

本节介绍如何将类型映射到Cassandra描述以及从Cassandra描述映射到类型。

Apache Cassandra的Spring Data支持Apache Cassandra提供的几种类型。除了这些类型之外,Apache Cassandra的Spring Data还提供了一组内置的转换器来映射其他类型。您可以提供自己的自定义转换器来调整类型转换。参见“[cassandra.mapping.explicit-converters]”以获取更多详细信息。下表将Spring数据类型映射到Cassandra类型:

Table 4. 类型

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

 (default), 

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数据类型。使用@CassandraType可以将Java类型映射到其他Cassandra类型,如下例所示:

Example 99. 枚举映射到数值类型

@Table
public class EnumToOrdinalMapping {

  @PrimaryKey String id;

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

public enum Condition {
  NEW, USED
}
           

14.3. 基于约定的映射

当没有提供额外的映射元数据时,MappingCassandraConverter使用一些约定将域对象映射到CQL表。规则是:

  • 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 named 

    savingsaccount

    . 简单Java类名通过更改为小写形式映射到表名。例如com.bigbank.SavingsAccount映射到名为savingsaccount的表。
  • The converter uses any registered Spring 

    Converter

     instances to override the default mapping of object properties to tables fields. 转换器使用任何注册的Spring转换器实例来覆盖对象属性到表字段的默认映射。
  • The properties of an object are used to convert to and from properties in the table. 对象的属性用于转换表中的属性。

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.

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

The following example shows how to configure a 

NamingStrategy

:

以下示例显示如何配置NamingStrategy:

Example 100. Configuring 

NamingStrategy

 on 

CassandraMappingContext

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));
           

继续阅读