Modeling Entities
本章介绍如何对实体建模并说明它们在 Couchbase Server 本身的对应表示。
This chapter describes how to model Entities and explains their counterpart representation in Couchbase Server itself.
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. |
Documents and Fields
所有实体都应该用 @Document
注释进行注释,但它不是必需的。
All entities should be annotated with the @Document
annotation, but it is not a requirement.
此外,实体中的每个字段都应该用 @Field
注释进行注释。虽然从严格意义上来说这是可选的,但是它有助于减少极端情况并明确显示实体的意图和设计。它还可以用于在不同的名称下存储字段。
Also, every field in the entity should be annotated with the @Field
annotation. While this is - strictly speaking -
optional, it helps to reduce edge cases and clearly shows the intent and design of the entity. It can also be used to
store the field under a different name.
还有一个特殊的 @Id
注释,它需要始终存在。最佳实践是还将属性命名为 id
。
There is also a special @Id
annotation which needs to be always in place. Best practice is to also name the property
id
.
这是一个非常简单的 User
实体:
Here is a very simple User
entity:
import org.springframework.data.annotation.Id;
import org.springframework.data.couchbase.core.mapping.Field;
import org.springframework.data.couchbase.core.mapping.Document;
@Document
public class User {
@Id
private String id;
@Field
private String firstname;
@Field
private String lastname;
public User(String id, String firstname, String lastname) {
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
}
public String getId() {
return id;
}
public String getFirstname() {
return firstname;
}
public String getLastname() {
return lastname;
}
}
Couchbase Server 支持文档的自动过期。该库通过 @Document
注释实现对它的支持。你可以设置 expiry
值,它转换成文档自动删除之前经过的秒数。如果你想让它在变更后 10 秒过期,就将它设置为 @Document(expiry = 10)
。或者,你可以使用 Spring 的属性支持和 expiryExpression
参数配置过期时间,以允许动态更改过期值。例如: @Document(expiryExpression = "${valid.document.expiry}")
。该属性必须可解析为 int 值,并且不能混合这两种方法。
Couchbase Server supports automatic expiration for documents.
The library implements support for it through the @Document
annotation.
You can set a expiry
value which translates to the number of seconds until the document gets removed automatically.
If you want to make it expire in 10 seconds after mutation, set it like @Document(expiry = 10)
.
Alternatively, you can configure the expiry using Spring’s property support and the expiryExpression
parameter, to allow for dynamically changing the expiry value.
For example: @Document(expiryExpression = "${valid.document.expiry}")
.
The property must be resolvable to an int value and the two approaches cannot be mixed.
如果你希望文档中的字段名称和实体中使用的字段名称有不同的表示,可以在 @Field
注释中设置不同的名称。例如,如果你希望让你的文档保持小巧,可以将 firstname 字段设置为 @Field("fname")
。在 JSON 文档中,你将看到 {"fname": ".."}
而不是 {"firstname": ".."}
。
If you want a different representation of the field name inside the document in contrast to the field name used in your entity, you can set a different name on the @Field
annotation.
For example if you want to keep your documents small you can set the firstname field to @Field("fname")
.
In the JSON document, you’ll see {"fname": ".."}
instead of {"firstname": ".."}
.
@Id
注释必须存在,因为 Couchbase 中的每个文档都需要一个唯一键。该键必须是任何具有最大 250 个字符长度的字符串。随意使用任何适合你的用例的键,无论是 UUID、电子邮件地址还是其他任何内容。
The @Id
annotation needs to be present because every document in Couchbase needs a unique key.
This key needs to be any string with a length of maximum 250 characters.
Feel free to use whatever fits your use case, be it a UUID, an email address or anything else.
对 Couchbase Server 存储桶的写入可以随意分配持久性要求;它指示 Couchbase Server 在集群中的多个节点的内存和/或磁盘位置更新指定文档,然后考虑提交写入。默认的持久性要求也可以通过 @Document
或 @Durability
注释进行配置。例如: @Document(durabilityLevel = DurabilityLevel.MAJORITY)
将强制将变更复制到大多数数据服务节点。这两个注释都支持通过 durabilityExpression
属性(注意 SPEL 不受支持)基于表达式的持久性级别分配。
Writes to Couchbase-Server buckets can optionally be assigned durability requirements; which instruct Couchbase Server to update the specified document on multiple nodes in memory and/or disk locations across the cluster; before considering the write to be committed.
Default durability requirements can also be configured through the @Document
or @Durability
annotations.
For example: @Document(durabilityLevel = DurabilityLevel.MAJORITY)
will force mutations to be replicated to a majority of the Data Service nodes. Both of the annotations support expression based durability level assignment via durabilityExpression
attribute (Note SPEL is not supported).
Datatypes and Converters
首选的存储格式是 JSON。它很好,但与许多数据表示一样,它允许的数据类型少于你可以在 Java 中直接表示的数据类型。因此,对于所有非基本类型,都需要以某种形式进行转换到受支持的类型和从受支持的类型转换。
The storage format of choice is JSON. It is great, but like many data representations it allows less datatypes than you could express in Java directly. Therefore, for all non-primitive types some form of conversion to and from supported types needs to happen.
对于以下实体字段类型,不需要添加特殊处理:
For the following entity field types, you don’t need to add special handling:
Java Type | JSON Representation |
---|---|
string |
string |
boolean |
boolean |
byte |
number |
short |
number |
int |
number |
long |
number |
float |
number |
double |
number |
null |
Ignored on write |
由于 JSON 支持对象(“maps”)和列表,所以可以自然地转换 Map
和 List
类型。如果它们仅包含上段落中的基本字段类型,则不需要添加特殊处理。以下是一个示例:
Since JSON supports objects ("maps") and lists, Map
and List
types can be converted naturally.
If they only contain primitive field types from the last paragraph, you don’t need to add special handling too.
Here is an example:
@Document
public class User {
@Id
private String id;
@Field
private List<String> firstnames;
@Field
private Map<String, Integer> childrenAges;
public User(String id, List<String> firstnames, Map<String, Integer> childrenAges) {
this.id = id;
this.firstnames = firstnames;
this.childrenAges = childrenAges;
}
}
将一个具有样本数据的用户存储为 JSON 表示可能如下所示:
Storing a user with some sample data could look like this as a JSON representation:
{
"_class": "foo.User",
"childrenAges": {
"Alice": 10,
"Bob": 5
},
"firstnames": [
"Foo",
"Bar",
"Baz"
]
}
你不必每次都将所有内容细分为基本类型和列表/映射。当然,你还可以从这些基本值中组合出其他对象。让我们修改最后一个示例,以便我们想要存储一个 Children
的 List
:
You don’t need to break everything down to primitive types and Lists/Maps all the time.
Of course, you can also compose other objects out of those primitive values.
Let’s modify the last example so that we want to store a List
of Children
:
@Document
public class User {
@Id
private String id;
@Field
private List<String> firstnames;
@Field
private List<Child> children;
public User(String id, List<String> firstnames, List<Child> children) {
this.id = id;
this.firstnames = firstnames;
this.children = children;
}
static class Child {
private String name;
private int age;
Child(String name, int age) {
this.name = name;
this.age = age;
}
}
}
一个填充后的对象可能如下所示:
A populated object can look like:
{
"_class": "foo.User",
"children": [
{
"age": 4,
"name": "Alice"
},
{
"age": 3,
"name": "Bob"
}
],
"firstnames": [
"Foo",
"Bar",
"Baz"
]
}
大多数情况下,你还需要存储诸如 Date
之类的临时值。由于它不能直接存储在 JSON 中,因此需要进行转换。该库为 Date
、Calendar
和 JodaTime 类型(如果在类路径中)实现了默认转换器。所有这些在文档中默认用 UNIX 时间戳(数字)表示。你可以使用后面显示的自定义转换器随时覆盖默认行为。以下是一个示例:
Most of the time, you also need to store a temporal value like a Date
.
Since it can’t be stored directly in JSON, a conversion needs to happen.
The library implements default converters for Date
, Calendar
and JodaTime types (if on the classpath).
All of those are represented by default in the document as a unix timestamp (number).
You can always override the default behavior with custom converters as shown later.
Here is an example:
@Document
public class BlogPost {
@Id
private String id;
@Field
private Date created;
@Field
private Calendar updated;
@Field
private String title;
public BlogPost(String id, Date created, Calendar updated, String title) {
this.id = id;
this.created = created;
this.updated = updated;
this.title = title;
}
}
一个填充后的对象可能如下所示:
A populated object can look like:
{
"title": "a blog post title",
"_class": "foo.BlogPost",
"updated": 1394610843,
"created": 1394610843897
}
可以选择通过将系统属性 org.springframework.data.couchbase.useISOStringConverterForDate
设置为 true,将日期转换到 ISO-8601 兼容的字符串或反之。如果你想覆盖转换器或实现你自己的转换器,这也是可能的。该库实现了通用的 Spring 转换器模式。你可以在配置中在 bean 创建时插入自定义转换器。以下是如何配置它(在被覆盖的 AbstractCouchbaseConfiguration
中):
Optionally, Date can be converted to and from ISO-8601 compliant strings by setting system property org.springframework.data.couchbase.useISOStringConverterForDate
to true.
If you want to override a converter or implement your own one, this is also possible.
The library implements the general Spring Converter pattern.
You can plug in custom converters on bean creation time in your configuration.
Here’s how you can configure it (in your overridden AbstractCouchbaseConfiguration
):
@Override
public CustomConversions customConversions() {
return new CustomConversions(Arrays.asList(FooToBarConverter.INSTANCE, BarToFooConverter.INSTANCE));
}
@WritingConverter
public static enum FooToBarConverter implements Converter<Foo, Bar> {
INSTANCE;
@Override
public Bar convert(Foo source) {
return /* do your conversion here */;
}
}
@ReadingConverter
public static enum BarToFooConverter implements Converter<Bar, Foo> {
INSTANCE;
@Override
public Foo convert(Bar source) {
return /* do your conversion here */;
}
}
在使用自定义转换时需要注意一些事项:
There are a few things to keep in mind with custom conversions:
-
To make it unambiguous, always use the
@WritingConverter
and@ReadingConverter
annotations on your converters. Especially if you are dealing with primitive type conversions, this will help to reduce possible wrong conversions. -
If you implement a writing converter, make sure to decode into primitive types, maps and lists only. If you need more complex object types, use the
CouchbaseDocument
andCouchbaseList
types, which are also understood by the underlying translation engine. Your best bet is to stick with as simple as possible conversions. -
Always put more special converters before generic converters to avoid the case where the wrong converter gets executed.
-
For dates, reading converters should be able to read from any
Number
(not justLong
). This is required for N1QL support.
Optimistic Locking
在某些情况下,当你对文档执行突变操作时,你可能希望确保你不会覆盖其他用户的更改。为此,你有三个选择:事务(自 Couchbase 6.5 起)、悲观并发(锁定)或乐观并发。
In certain situations you may want to ensure that you are not overwriting another users changes when you perform a mutation operation on a document. For this you have three choices: Transactions (since Couchbase 6.5), pessimistic concurrency (locking) or optimistic concurrency.
乐观并发往往比悲观并发或事务提供更好的性能,因为不会真正锁定数据,并且不会存储有关操作的额外信息(没有事务日志)。
Optimistic concurrency tends to provide better performance than pessimistic concurrency or transactions, because no actual locks are held on the data and no extra information is stored about the operation (no transaction log).
为了实现乐观锁,Couchbase 使用 CAS(比较并交换)方法。当一个文档发生突变时,CAS 值也会改变。CAS 对客户端是不可见的,唯一需要知道的是,当内容或元信息发生更改时它也会更改。
To implement optimistic locking, Couchbase uses a CAS (compare and swap) approach. When a document is mutated, the CAS value also changes. The CAS is opaque to the client, the only thing you need to know is that it changes when the content or a meta information changes too.
在其他数据存储中,可以通过递增计数器的任意版本字段实现类似的行为。由于 Couchbase 以一种更好的方式支持此操作,因此很容易实现。如果你想要自动乐观锁支持,你需要做的就是给一个长字段添加一个 @Version
注解,如下所示:
In other datastores, similar behavior can be achieved through an arbitrary version field with a incrementing counter.
Since Couchbase supports this in a much better fashion, it is easy to implement.
If you want automatic optimistic locking support, all you need to do is add a @Version
annotation on a long field like this:
@Document
public class User {
@Version
private long version;
// constructor, getters, setters...
}
如果你通过模板或存储库加载一个文档,版本字段将自动填充为当前 CAS 值。重要的是要注意,你不应访问该字段或自行更改它。一旦你将文档保存回去,它将成功或失败,并引发 OptimisticLockingFailureException
。如果你遇到这样的异常,则进一步的方法取决于你希望应用实现什么。你应该重试整个加载-更新-写入周期或将错误传播到上层以进行适当处理。
If you load a document through the template or repository, the version field will be automatically populated with the current CAS value.
It is important to note that you shouldn’t access the field or even change it on your own.
Once you save the document back, it will either succeed or fail with a OptimisticLockingFailureException
.
If you get such an exception, the further approach depends on what you want to achieve application wise.
You should either retry the complete load-update-write cycle or propagate the error to the upper layers for proper handling.
Validation
该库支持 JSR 303 验证,它基于实体中的直接注解。当然,你可以在服务层中添加各种验证,但这种方法很好地耦合到了你的实际实体中。
The library supports JSR 303 validation, which is based on annotations directly in your entities. Of course you can add all kinds of validation in your service layer, but this way its nicely coupled to your actual entities.
要使其正常工作,你需要包含两个其他依赖项。JSR 303 和实现它的库,例如 Hibernate 支持的那个:
To make it work, you need to include two additional dependencies. JSR 303 and a library that implements it, like the one supported by hibernate:
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
现在你需要向配置添加两个 bean:
Now you need to add two beans to your configuration:
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
@Bean
public ValidatingCouchbaseEventListener validationEventListener() {
return new ValidatingCouchbaseEventListener(validator());
}
现在你可以使用 JSR 303 注解为你的字段添加注解。如果 save()
上的验证失败,则会抛出 ConstraintViolationException
。
Now you can annotate your fields with JSR303 annotations.
If a validation on save()
fails, a ConstraintViolationException
is thrown.
@Size(min = 10)
@Field
private String name;
Auditing
实体可以通过 Spring Data 审计机制自动审计(追踪哪个用户创建了对象、更新了对象以及在什么时候)。
Entities can be automatically audited (tracing which user created the object, updated the object, and at what times) through Spring Data auditing mechanisms.
首先,请注意,只有带有 @Version
注释字段的实体才能进行创建审计(否则框架会将创建解释为更新)。
First, note that only entities that have a @Version
annotated field can be audited for creation (otherwise the framework will interpret a creation as an update).
审计通过用 @CreatedBy
、@CreatedDate
、@LastModifiedBy
和 @LastModifiedDate
注释字段来工作。在持久化实体时,框架会自动向这些字段注入正确的值。xxxDate 注解必须放在 Date
字段(或兼容的 jodatime 类)上,而 xxxBy 注解可以放在任何类 T
的字段上(尽管两个字段必须是相同类型)。
Auditing works by annotating fields with @CreatedBy
, @CreatedDate
, @LastModifiedBy
and @LastModifiedDate
.
The framework will automatically inject the correct values on those fields when persisting the entity.
The xxxDate annotations must be put on a Date
field (or compatible, eg. jodatime classes) while the xxxBy annotations can be put on fields of any class T
(albeit both fields must be of the same type).
要配置审计,首先你需要在上下文中有一个审计感知 bean。所述 bean 必须为 AuditorAware<T>
类型(允许生成一个值,该值可以存储在我们之前看到的类型 T
的 xxxBy 字段中)。其次,你必须通过使用 @EnableCouchbaseAuditing
注解在 @Configuration
类中激活审计。
To configure auditing, first you need to have an auditor aware bean in the context.
Said bean must be of type AuditorAware<T>
(allowing to produce a value that can be stored in the xxxBy fields of type T
we saw earlier).
Secondly, you must activate auditing in your @Configuration
class by using the @EnableCouchbaseAuditing
annotation.
这是一个示例:
Here is an example:
@Document
public class AuditedItem {
@Id
private final String id;
private String value;
@CreatedBy
private String creator;
@LastModifiedBy
private String lastModifiedBy;
@LastModifiedDate
private Date lastModification;
@CreatedDate
private Date creationDate;
@Version
private long version;
//..omitted constructor/getters/setters/...
}
请注意,@CreatedBy
和 @LastModifiedBy
都放在 String
字段上,因此我们的 AuditorAware
必须使用 String
。
Notice both @CreatedBy
and @LastModifiedBy
are both put on a String
field, so our AuditorAware
must work with String
.
public class NaiveAuditorAware implements AuditorAware<String> {
private String auditor = "auditor";
@Override
public String getCurrentAuditor() {
return auditor;
}
public void setAuditor(String auditor) {
this.auditor = auditor;
}
}
为了将所有这些联系在一起,我们使用 Java 配置来声明 AuditorAware bean 以及激活审核:
To tie all that together, we use the java configuration both to declare an AuditorAware bean and to activate auditing:
@Configuration
@EnableCouchbaseAuditing //this activates auditing
public class AuditConfiguration extends AbstractCouchbaseConfiguration {
//... a few abstract methods omitted here
// this creates the auditor aware bean that will feed the annotations
@Bean
public NaiveAuditorAware testAuditorAware() {
return new NaiveAuditorAware();
}