此版本仍处于开发阶段,尚未被视为稳定版本。如需使用最新稳定版本,请采用 Spring Data Couchbase 6.0.4spring-doc.cadn.net.cn

建模实体

本章介绍如何对实体进行建模,并解释它们在 Couchbase Server 中的对应表示方式。spring-doc.cadn.net.cn

对象映射基础

本节介绍 Spring Data 对象映射、对象创建、字段和属性访问、可变性与不可变性的基础知识。 请注意,本节仅适用于不使用底层数据存储对象映射(如 JPA)的 Spring Data 模块。 此外,请务必查阅特定于存储的章节,以了解特定于存储的对象映射内容,例如索引、自定义列名或字段名等。spring-doc.cadn.net.cn

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

  1. 通过使用公开的构造函数之一来创建实例。spring-doc.cadn.net.cn

  2. 实例填充以实例化所有公开的属性。spring-doc.cadn.net.cn

对象创建

Spring Data 会自动尝试检测持久化实体的构造函数,以用于实例化该类型的对象。 解析算法的工作方式如下:spring-doc.cadn.net.cn

  1. 如果存在一个使用 @PersistenceCreator 注解的静态工厂方法,则会使用该方法。spring-doc.cadn.net.cn

  2. 如果只有一个构造函数,则使用该构造函数。spring-doc.cadn.net.cn

  3. 如果存在多个构造函数,并且恰好有一个被标注为@PersistenceCreator,那么将使用该构造函数。spring-doc.cadn.net.cn

  4. 如果该类型是一个 Java Record,则使用其规范构造函数。spring-doc.cadn.net.cn

  5. 如果存在无参构造函数,则会使用它。 其他构造函数将被忽略。spring-doc.cadn.net.cn

值解析假定构造函数/工厂方法的参数名称与实体的属性名称相匹配,即解析过程将如同要填充该属性一样进行,包括映射中的所有自定义设置(例如不同的数据存储列名或字段名等)。 这也要求类文件中包含参数名称信息,或者在构造函数上存在 @ConstructorProperties 注解。spring-doc.cadn.net.cn

可以通过使用 Spring Framework 的 @Value 注解并结合特定于存储的 SpEL 表达式来自定义值解析。 有关更多详细信息,请参阅关于特定于存储的映射的相关章节。spring-doc.cadn.net.cn

对象创建内部机制

为了避免反射带来的开销,Spring Data 默认使用在运行时生成的工厂类来创建对象,该工厂类会直接调用领域类的构造函数。 例如,对于以下示例类型:spring-doc.cadn.net.cn

class Person {
  Person(String firstname, String lastname) { … }
}

我们将在运行时创建一个语义上与此类等价的工厂类:spring-doc.cadn.net.cn

class PersonObjectInstantiator implements ObjectInstantiator {

  Object newInstance(Object... args) {
    return new Person((String) args[0], (String) args[1]);
  }
}

这使我们相比反射获得了大约10%的性能提升。 要使领域类能够享受此类优化,它需要满足一组约束条件:spring-doc.cadn.net.cn

如果满足上述任一条件,Spring Data 将回退到通过反射来实例化实体。spring-doc.cadn.net.cn

属性填充

一旦实体实例被创建,Spring Data 就会填充该类中所有其余的持久化属性。 除非标识符属性已经由实体的构造函数填充(即通过其构造函数参数列表传入),否则将首先填充标识符属性,以便解析循环对象引用。 之后,所有尚未通过构造函数填充的非瞬态(non-transient)属性都会被设置到实体实例上。 为此,我们使用以下算法:spring-doc.cadn.net.cn

  1. 如果该属性是不可变的,但提供了一个 with… 方法(见下文),我们将使用该 with… 方法创建一个包含新属性值的新实体实例。spring-doc.cadn.net.cn

  2. 如果定义了属性访问(即通过 getter 和 setter 进行访问),我们将调用 setter 方法。spring-doc.cadn.net.cn

  3. 如果该属性是可变的,我们会直接设置字段。spring-doc.cadn.net.cn

  4. 如果该属性是不可变的,我们将使用构造函数(由持久化操作调用,参见对象创建)来创建该实例的一个副本。spring-doc.cadn.net.cn

  5. 默认情况下,我们直接设置字段值。spring-doc.cadn.net.cn

属性填充内部机制

与我们在对象构造中的优化类似,我们还使用 Spring Data 在运行时生成的访问器类来与实体实例进行交互。spring-doc.cadn.net.cn

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.lastname);
  }

  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 持有底层对象的一个可变实例,以便对原本不可变的属性进行修改。
2 默认情况下,Spring Data 使用字段访问(field-access)来读取和写入属性值。根据 private 字段的可见性规则,使用 MethodHandles 来与字段进行交互。
3 该类提供了一个 withId(…) 方法,用于设置标识符,例如当一个实例被插入到数据存储中并生成了标识符时。调用 withId(…) 会创建一个新的 Person 对象。所有后续的修改都将在新实例上进行,而之前的实例保持不变。
4 使用属性访问(property-access)允许直接调用方法,而无需使用 MethodHandles

这使我们相比反射获得了大约25%的性能提升。 要使领域类能够享受此类优化,它需要满足一组约束条件:spring-doc.cadn.net.cn

默认情况下,Spring Data 会尝试使用生成的属性访问器,如果检测到限制,则回退到基于反射的访问器。spring-doc.cadn.net.cn

让我们来看一下以下实体:spring-doc.cadn.net.cn

一个示例实体
class Person {

  private final @Id Long id;                                                (1)
  private final String firstname, lastname;                                 (2)
  private final LocalDate birthday;
  private final int age;                                                    (3)

  private String comment;                                                   (4)
  private @AccessType(Type.PROPERTY) String remarks;                        (5)

  static Person of(String firstname, String lastname, LocalDate birthday) { (6)

    return new Person(null, firstname, lastname, birthday,
      Period.between(birthday, LocalDate.now()).getYears());
  }

  Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6)

    this.id = id;
    this.firstname = firstname;
    this.lastname = lastname;
    this.birthday = birthday;
    this.age = age;
  }

  Person withId(Long id) {                                                  (1)
    return new Person(id, this.firstname, this.lastname, this.birthday, this.age);
  }

  void setRemarks(String remarks) {                                         (5)
    this.remarks = remarks;
  }
}
1 标识符属性是 final 的,但在构造函数中被设为 null。 该类提供了一个 withId(…) 方法,用于设置标识符,例如当实例被插入到数据存储中并生成了标识符时。 原始的 Person 实例保持不变,因为会创建一个新的实例。 对于其他由存储管理、但在持久化操作中可能需要更改的属性,通常也采用相同的模式。 Wither 方法是可选的,因为持久化构造函数(参见第6节)实际上是一个拷贝构造函数,设置该属性会被转换为创建一个应用了新标识符值的新实例。
2 firstnamelastname 属性是普通的不可变属性,可能通过 getter 方法对外暴露。
3 age 属性是一个不可变的派生属性,它源自 birthday 属性。 按照所示的设计,数据库中的值将优先于默认值,因为 Spring Data 会使用唯一声明的构造函数。 即使本意是优先采用计算得出的值,该构造函数也必须将 age 作为参数(即使可能忽略它),否则属性填充步骤会尝试设置 age 字段,但由于该字段不可变且不存在 with… 方法,从而导致失败。
4 comment 属性是可变的,通过直接设置其字段进行赋值。
5 remarks 属性是可变的,通过调用其 setter 方法进行赋值。
6 该类提供了一个工厂方法和一个构造函数用于对象创建。 这里的核心思想是使用工厂方法而非额外的构造函数,以避免通过 @PersistenceCreator 进行构造函数消歧。 相反,属性的默认值处理在工厂方法内部完成。 如果你想让 Spring Data 使用该工厂方法进行对象实例化,请使用 @PersistenceCreator 注解该方法。

通用建议

  • 尽量使用不可变对象 — 不可变对象的创建非常直接,因为实例化对象只需调用其构造函数即可。 此外,这样做可以避免在你的领域对象中充斥着大量 setter 方法,从而防止客户端代码随意修改对象的状态。 如果你确实需要这些 setter 方法,建议将其设为包级私有(package protected),以便只有有限数量的同包类才能调用它们。 仅通过构造函数进行对象实例化,比通过属性赋值的方式快多达 30%。spring-doc.cadn.net.cn

  • 提供一个包含所有参数的构造函数 — 即使你无法或不希望将实体建模为不可变值,仍然建议提供一个接收实体所有属性(包括可变属性)作为参数的构造函数,因为这样可以让对象映射跳过属性填充步骤,从而实现最佳性能。spring-doc.cadn.net.cn

  • 使用工厂方法替代重载构造函数,以避免 @PersistenceCreator — 由于需要全参数构造函数以实现最佳性能,我们通常希望暴露更多针对特定应用场景的构造函数,这些构造函数会省略自动生成的标识符等内容。 采用静态工厂方法来暴露这些全参数构造函数的变体,是一种既定的设计模式。spring-doc.cadn.net.cn

  • 请确保遵守相关约束条件,以允许使用所生成的实例化器和属性访问器类 —spring-doc.cadn.net.cn

  • 若要生成标识符,仍应使用 final 字段结合全参数持久化构造函数(推荐)或 with… 方法 —spring-doc.cadn.net.cn

  • 使用 Lombok 避免样板代码 — 由于持久化操作通常需要一个包含所有参数的构造函数,其声明往往会变成繁琐的样板代码,即重复地将参数赋值给字段。通过使用 Lombok 的 @AllArgsConstructor 注解,可以很好地避免这种情况。spring-doc.cadn.net.cn

覆盖属性

Java 允许对领域类进行灵活的设计,其中子类可以定义一个与其超类中已声明的同名属性。 请考虑以下示例:spring-doc.cadn.net.cn

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 会遮蔽(shadow)SuperType.field。 根据类的设计,使用构造函数可能是设置 SuperType.field 的唯一默认方法。 或者,也可以在 setter 方法中调用 super.setField(…) 来设置 field 中的 SuperType。 所有这些机制在某种程度上都会产生冲突,因为这些属性具有相同的名称,却可能代表两个不同的值。 如果类型不可赋值,Spring Data 会跳过父类的属性。 也就是说,只有当被重写的属性类型可赋值给其父类属性类型时,才会被注册为重写;否则,父类属性将被视为瞬态(transient)。 我们通常建议使用不同的属性名称。spring-doc.cadn.net.cn

Spring Data 模块通常支持被重写的属性持有不同的值。 从编程模型的角度来看,有几点需要注意:spring-doc.cadn.net.cn

  1. 应持久化哪些属性(默认为所有已声明的属性)? 您可以通过使用 @Transient 注解来排除某些属性。spring-doc.cadn.net.cn

  2. 如何在您的数据存储中表示属性? 对不同的值使用相同的字段/列名通常会导致数据损坏,因此您应使用显式的字段/列名对至少其中一个属性进行注解。spring-doc.cadn.net.cn

  3. 使用 @AccessType(PROPERTY) 是不可行的,因为通常无法在不对 setter 实现做进一步假设的情况下设置超类属性。spring-doc.cadn.net.cn

Kotlin 支持

Spring Data 适配了 Kotlin 的特性,以支持对象的创建和变更。spring-doc.cadn.net.cn

Kotlin 对象创建

Kotlin 类支持被实例化,所有类默认都是不可变的,并且需要显式声明属性以定义可变属性。spring-doc.cadn.net.cn

Spring Data 会自动尝试检测持久化实体的构造函数,以用于实例化该类型的对象。 解析算法的工作方式如下:spring-doc.cadn.net.cn

  1. 如果存在一个使用 @PersistenceCreator 注解的构造函数,则会使用该构造函数。spring-doc.cadn.net.cn

  2. 如果该类型是一个Kotlin 数据类,则使用其主构造函数。spring-doc.cadn.net.cn

  3. 如果存在一个使用 @PersistenceCreator 注解的静态工厂方法,则会使用该方法。spring-doc.cadn.net.cn

  4. 如果只有一个构造函数,则使用该构造函数。spring-doc.cadn.net.cn

  5. 如果存在多个构造函数,并且恰好有一个被标注为@PersistenceCreator,那么将使用该构造函数。spring-doc.cadn.net.cn

  6. 如果该类型是一个 Java Record,则使用其规范构造函数。spring-doc.cadn.net.cn

  7. 如果存在无参构造函数,则会使用它。 其他构造函数将被忽略。spring-doc.cadn.net.cn

考虑以下 dataPersonspring-doc.cadn.net.cn

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

上面的类会编译成一个带有显式构造函数的典型类。我们可以通过添加另一个构造函数并使用 @PersistenceCreator 注解来定制此类,以指明首选的构造函数:spring-doc.cadn.net.cn

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

    @PersistenceCreator
    constructor(id: String) : this(id, "unknown")
}

Kotlin 通过允许在未提供参数时使用默认值来支持参数的可选性。 当 Spring Data 检测到一个带有参数默认值的构造函数时,如果数据存储未提供该值(或仅返回 null),它就会省略这些参数,从而让 Kotlin 能够应用参数默认值。请考虑以下对 name 参数应用默认值的类:spring-doc.cadn.net.cn

data class Person(var id: String, val name: String = "unknown")

每当 name 参数不在结果中,或者其值为 null 时,name 将默认为 unknownspring-doc.cadn.net.cn

Spring Data 不支持委托属性。映射元数据会过滤 Kotlin 数据类中的委托属性。 在其他所有情况下,您可以通过使用 @Transient 注解属性来排除委托属性的合成字段。

Kotlin 数据类的属性填充

在 Kotlin 中,所有类默认都是不可变的,需要显式声明属性才能定义可变属性。 请考虑以下 dataPersonspring-doc.cadn.net.cn

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

该类实际上是不可变的。 它允许创建新实例,因为 Kotlin 会生成一个 copy(…) 方法,该方法通过复制现有对象的所有属性值并应用作为方法参数提供的属性值来创建新的对象实例。spring-doc.cadn.net.cn

Kotlin 重写属性

Kotlin 允许声明属性重写,以在子类中修改属性。spring-doc.cadn.net.cn

open class SuperType(open var field: Int)

class SubType(override var field: Int = 1) :
	SuperType(field) {
}

这种安排会导致两个名为field的属性。 Kotlin 会为每个类中的每个属性生成属性访问器(getter 和 setter)。 实际上,代码看起来如下所示:spring-doc.cadn.net.cn

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 添加一个方法,通过 SuperType.field 来设置 this.SuperType.field = …,但这超出了所支持的约定范围。 属性重写在一定程度上会造成冲突,因为这些属性名称相同,却可能代表两个不同的值。 我们通常建议使用不同的属性名称。spring-doc.cadn.net.cn

Spring Data 模块通常支持被重写的属性持有不同的值。 从编程模型的角度来看,有几点需要注意:spring-doc.cadn.net.cn

  1. 应持久化哪些属性(默认为所有已声明的属性)? 您可以通过使用 @Transient 注解来排除某些属性。spring-doc.cadn.net.cn

  2. 如何在您的数据存储中表示属性? 对不同的值使用相同的字段/列名通常会导致数据损坏,因此您应使用显式的字段/列名对至少其中一个属性进行注解。spring-doc.cadn.net.cn

  3. 使用 @AccessType(PROPERTY) 是不可行的,因为无法设置超类属性。spring-doc.cadn.net.cn

Kotlin 值类

Kotlin 值类(Value Classes)旨在构建更具表达力的领域模型,以显式地体现底层概念。 Spring Data 能够读取和写入使用值类定义属性的类型。spring-doc.cadn.net.cn

考虑以下领域模型:spring-doc.cadn.net.cn

@JvmInline
value class EmailAddress(val theAddress: String)                                    (1)

data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) (2)
1 一个具有不可为空值类型的简单值类。
2 使用 EmailAddress 值类定义属性的数据类。
使用非基本值类型的不可为空属性在编译后的类中会被展平为该值类型。 可为空的基本值类型或值类型中的可为空值类型则以其包装类型表示,这会影响值类型在数据库中的表示方式。

文档和字段

所有实体都应使用 @Document 注解进行标注,但这并非强制要求。spring-doc.cadn.net.cn

此外,实体中的每个字段都应使用 @Field 注解进行标注。虽然严格来说这是可选的,但它有助于减少边缘情况,并清晰地展示实体的意图和设计。它还可用于将字段以不同的名称存储。spring-doc.cadn.net.cn

还有一个特殊的 @Id 注解必须始终存在。最佳实践是将该属性命名为 idspring-doc.cadn.net.cn

这是一个非常简单的 User 实体:spring-doc.cadn.net.cn

示例 1. 一个简单的带有字段的文档
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}")。 该属性必须能够解析为整数值,且这两种方法不能混用。spring-doc.cadn.net.cn

如果您希望在文档中的字段名称表示与实体中使用的字段名称不同,您可以在 @Field 注解上设置不同的名称。 例如,如果您希望保持文档较小,可以将 firstname 字段设置为 @Field("fname")。 在 JSON 文档中,您将看到 {"fname": ".."} 而不是 {"firstname": ".."}spring-doc.cadn.net.cn

The @Id 注解必须存在,因为 Couchbase 中的每个文档都需要一个唯一的键。 此键必须是长度最大为 250 个字符的任意字符串。 请根据您的使用场景自由选择合适的值,例如 UUID、电子邮件地址或其他任何内容。spring-doc.cadn.net.cn

写入 Couchbase-Server 存储桶的操作可以可选地分配持久性要求;这指示 Couchbase Server 在将写入操作视为已提交之前,在集群中的多个节点上的内存和/或磁盘位置更新指定的文档。 默认持久性要求也可以通过 @Document@Durability 注解进行配置。 例如:@Document(durabilityLevel = DurabilityLevel.MAJORITY) 将强制将突变复制到大多数 Data Service 节点。两个注解都支持通过 durabilityExpression 属性基于表达式分配持久性级别(注意:不支持 SPEL)。spring-doc.cadn.net.cn

数据类型和转换器

首选的存储格式是 JSON。它非常棒,但像许多数据表示一样,它允许的数据类型少于你直接在 Java 中能够表达的类型。 因此,对于所有非基本类型,需要发生某种形式的受支持类型之间的转换。spring-doc.cadn.net.cn

对于以下实体字段类型,您无需添加特殊处理:spring-doc.cadn.net.cn

表 1. 基本类型
Java 类型 JSON 表示

字符串spring-doc.cadn.net.cn

字符串spring-doc.cadn.net.cn

布尔值spring-doc.cadn.net.cn

布尔值spring-doc.cadn.net.cn

字节spring-doc.cadn.net.cn

数字spring-doc.cadn.net.cn

简短spring-doc.cadn.net.cn

数字spring-doc.cadn.net.cn

整型spring-doc.cadn.net.cn

数字spring-doc.cadn.net.cn

长整型spring-doc.cadn.net.cn

数字spring-doc.cadn.net.cn

浮动spring-doc.cadn.net.cn

数字spring-doc.cadn.net.cn

doublespring-doc.cadn.net.cn

数字spring-doc.cadn.net.cn

nullspring-doc.cadn.net.cn

写入时忽略spring-doc.cadn.net.cn

由于 JSON 支持对象("映射")和列表,MapList 类型可以自然地转换。 如果它们仅包含上一段中提到的基本字段类型,则无需添加特殊处理。 以下是一个示例:spring-doc.cadn.net.cn

示例 2. 一个包含 Map 和 List 的文档
@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 表示形式可能如下所示:spring-doc.cadn.net.cn

示例 3. 包含 Map 和 List 的文档 - JSON
{
    "_class": "foo.User",
    "childrenAges": {
        "Alice": 10,
        "Bob": 5
    },
    "firstnames": [
        "Foo",
        "Bar",
        "Baz"
    ]
}

你不必总是将所有内容分解为基本类型以及 Lists/Maps。 当然,你也可以利用这些基本值来组合其他对象。 让我们修改上一个示例,以便我们想要存储一个 ListChildrenspring-doc.cadn.net.cn

示例 4. 一个由复合对象组成的文档
@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;
        }

    }

}

一个已填充的对象可能如下所示:spring-doc.cadn.net.cn

示例 5. 一个由复合对象组成的文档 - JSON
{
  "_class": "foo.User",
  "children": [
    {
      "age": 4,
      "name": "Alice"
    },
    {
      "age": 3,
      "name": "Bob"
    }
  ],
  "firstnames": [
    "Foo",
    "Bar",
    "Baz"
  ]
}

大多数情况下,您还需要存储像 Date 这样的临时值。 由于它不能直接存储在 JSON 中,因此需要进行转换。 该库为 DateCalendar 以及 JodaTime 类型(如果在类路径上)实现了默认转换器。 所有这些类型在文档中默认都表示为 Unix 时间戳(数字)。 您可以随时使用自定义转换器覆盖默认行为,如下文所示。 以下是一个示例:spring-doc.cadn.net.cn

示例 6. 包含日期和日历的文档
@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;
    }

}

一个已填充的对象可能如下所示:spring-doc.cadn.net.cn

示例 7. 包含日期和日历的文档 - JSON
{
  "title": "a blog post title",
  "_class": "foo.BlogPost",
  "updated": 1394610843,
  "created": 1394610843897
}

可选地,通过将系统属性 org.springframework.data.couchbase.useISOStringConverterForDate 设置为 true,可以将 Date 转换为符合 ISO-8601 规范的字符串或从该类字符串转换。 如果您想要覆盖某个转换器或实现自己的转换器,这也是可行的。 该库实现了通用的 Spring Converter 模式。 您可以在配置中的 Bean 创建时插入自定义转换器。 以下是如何在您重写的 AbstractCouchbaseConfiguration 中进行配置的:spring-doc.cadn.net.cn

示例 8. 自定义转换器
@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 */;
    }

}

在进行自定义转换时,有几点需要注意:spring-doc.cadn.net.cn

  • 为明确起见,请始终在您的转换器上使用 @WritingConverter@ReadingConverter 注解。 特别是当您处理原始类型转换时,这将有助于减少可能的错误转换。spring-doc.cadn.net.cn

  • 如果您实现了编写转换器,请确保仅解码为基本类型、映射和列表。 如果您需要更复杂的对象类型,请使用 CouchbaseDocumentCouchbaseList 类型,底层翻译引擎也支持这些类型。 最佳策略是坚持尽可能简单的转换方式。spring-doc.cadn.net.cn

  • 始终将更特殊的转换器放在通用转换器之前,以避免错误执行了不合适的转换器。spring-doc.cadn.net.cn

  • 对于日期,读取转换器应能够读取任何 Number(而不仅仅是 Long)。 这是 N1QL 支持所必需的。spring-doc.cadn.net.cn

乐观锁

在某些情况下,您可能希望确保在对文档执行变更操作时不会覆盖其他用户的更改。为此,您有三种选择:事务(自 Couchbase 6.5 起)、悲观并发(锁定)或乐观并发。spring-doc.cadn.net.cn

乐观并发通常比悲观并发或事务提供更好的性能,因为它不对数据持有实际的锁,也不存储关于操作的额外信息(无事务日志)。spring-doc.cadn.net.cn

要实现乐观锁,Couchbase 使用 CAS(比较并交换)方法。当文档发生修改时,CAS 值也会随之改变。 CAS 对客户端是透明的,您只需知道当内容或元信息发生变化时,CAS 值也会更新。spring-doc.cadn.net.cn

在其他数据存储中,可以通过带有递增计数器的任意版本字段来实现类似的行为。 由于 Couchbase 以更好的方式支持此功能,因此实现起来非常简单。 如果您希望自动支持乐观锁,只需在一个 long 类型的字段上添加 @Version 注解即可,如下所示:spring-doc.cadn.net.cn

示例 9. 一个使用乐观锁的文档。
@Document
public class User {

        @Version
        private long version;

        // constructor, getters, setters...
}

如果您通过模板或存储库加载文档,version 字段将自动填充为当前的 CAS 值。 需要注意的是,您不应自行访问该字段或对其进行修改。 一旦您将文档保存回去,操作要么成功,要么以 OptimisticLockingFailureException 失败。 如果出现此类异常,后续处理方式取决于您在应用层面希望实现的目标。 您应该重试完整的加载 - 更新 - 写入周期,或者将错误传播到上层以便进行适当处理。spring-doc.cadn.net.cn

验证

该库支持 JSR 303 验证,它基于直接嵌入实体中的注解。 当然,你可以在服务层添加各种验证逻辑,但这种方式能使其与你的实际实体良好耦合。spring-doc.cadn.net.cn

要使功能正常工作,您需要包含两个额外的依赖项。 JSR 303 以及实现它的库,例如 Hibernate 支持的库:spring-doc.cadn.net.cn

示例 10. 验证依赖关系
<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
</dependency>

现在您需要向配置中添加两个 bean:spring-doc.cadn.net.cn

示例 11. 验证 Bean
@Bean
public LocalValidatorFactoryBean validator() {
    return new LocalValidatorFactoryBean();
}

@Bean
public ValidatingCouchbaseEventListener validationEventListener() {
    return new ValidatingCouchbaseEventListener(validator());
}

现在您可以使用 JSR303 注解来标注您的字段。 如果针对 save() 的验证失败,将抛出 ConstraintViolationExceptionspring-doc.cadn.net.cn

示例 12. 样本验证注解
@Size(min = 10)
@Field
private String name;

审计

实体可以通过 Spring Data 审计机制自动进行审计(追踪哪个用户创建了对象、更新了对象以及更新的时间)。spring-doc.cadn.net.cn

首先,请注意,只有具有 @Version 注解字段的实体才能被审计其创建操作(否则框架会将创建解释为更新)。spring-doc.cadn.net.cn

审计功能通过为字段添加@CreatedBy@CreatedDate@LastModifiedBy@LastModifiedDate注解来实现。 在持久化实体时,框架会自动向这些字段注入正确的值。 xxxDate 注解必须放在Date类型的字段上(或兼容类型,例如 jodatime 类),而 xxxBy 注解可以放在任何T类型的字段上(尽管两个字段必须是相同的类型)。spring-doc.cadn.net.cn

要配置审计功能,首先需要在上下文中拥有一个感知审计员的 Bean。 该 Bean 的类型必须是 AuditorAware<T>(以便能够生成一个值,该值可存储到之前看到的类型为 T 的 xxxBy 字段中)。 其次,您必须在您的 @Configuration 类中通过 @EnableCouchbaseAuditing 注解来启用审计功能。spring-doc.cadn.net.cn

这是一个示例:spring-doc.cadn.net.cn

示例 13. 样本审计实体
@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 协同工作。spring-doc.cadn.net.cn

示例 14. 示例 AuditorAware 实现
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 并激活审计功能:spring-doc.cadn.net.cn

示例 15. 样本审计配置
@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();
    }