Modeling Entities

本章介绍如何对实体建模并说明它们在 Couchbase Server 本身的对应表示。

Object Mapping Fundamentals

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

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

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

Object creation

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

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

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

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

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

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

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

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

Object creation internals

为了避免反射时间开销,Spring Data 对象创建在默认情况下使用在运行时生成的工厂类,该类将直接调用领域类的构造函数。对于以下示例类型:

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

我们将在运行时为这个类创建一个语义上等价的工厂类:

class PersonObjectInstantiator implements ObjectInstantiator {

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

这给了我们超过反射 10% 的迂回式性能提升。为了使领域类适用于这种优化,它需要遵守一组约束:

  • 它不得是私有类

  • 它不得是非静态内部类

  • 它不得是 CGLib 代理类

  • Spring Data 使用的构造函数不得为私有

如果符合上述任何一个标准,Spring Data 将回退到通过反射进行实体实例化。

Property population

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

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

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

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

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

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

Property population internals

类似于我们在对象构造中的优化,映射.object-creation.details,我们还使用 Spring Data 运行时生成的访问器类与实体实例进行交互。

class Person {

  private final Long id;
  private String firstname;
  private @AccessType(Type.PROPERTY) String lastname;

  Person() {
    this.id = null;
  }

  Person(Long id, String firstname, String lastname) {
    // Field assignments
  }

  Person withId(Long id) {
    return new Person(id, this.firstname, this.lastame);
  }

  void setLastname(String lastname) {
    this.lastname = lastname;
  }
}
A generated Property Accessor
class PersonPropertyAccessor implements PersistentPropertyAccessor {

  private static final MethodHandle firstname;              2

  private Person person;                                    1

  public void setProperty(PersistentProperty property, Object value) {

    String name = property.getName();

    if ("firstname".equals(name)) {
      firstname.invoke(person, (String) value);             2
    } else if ("id".equals(name)) {
      this.person = person.withId((Long) value);            3
    } else if ("lastname".equals(name)) {
      this.person.setLastname((String) value);              4
    }
  }
}
1 PropertyAccessor 持有底层对象的不可变实例。这是为了允许对其他不可变属性进行突变。
2 默认情况下,Spring Data 使用字段访问来读写属性值。根据 private 域的可见性规则,MethodHandles 用于与域进行交互。
3 类公开一个 withId(…) 方法,该方法用于设置标识符,例如,当将一个实例插入到数据存储中并且生成了一个标识符时。调用 withId(…) 创建一个新的 Person 对象。所有后续的突变将在新实例中进行,而不会影响前面的实例。
4 使用属性访问允许直接调用方法,而不使用 MethodHandles

这给了我们超过反射 25% 的迂回式性能提升。为了使领域类适用于这种优化,它需要遵守一组约束:

  • 类型不得驻留在默认值或 java 包之下。

  • 类型及其构造函数必须为 public

  • 类型为内部类的类型必须为 static

  • 使用的 Java 运行时必须允许在原始 ClassLoader 中声明类。Java 9 及更新版本施加了某些限制。

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

让我们看看以下实体:

A sample entity
class Person {

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

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

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

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

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

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

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

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

General recommendations

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

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

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

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

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

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

Overriding Properties

Java 允许对领域类进行灵活的设计,其中子类可以定义在超类中已经使用相同名称声明的属性。考虑以下示例:

public class SuperType {

   private CharSequence field;

   public SuperType(CharSequence field) {
      this.field = field;
   }

   public CharSequence getField() {
      return this.field;
   }

   public void setField(CharSequence field) {
      this.field = field;
   }
}

public class SubType extends SuperType {

   private String field;

   public SubType(String field) {
      super(field);
      this.field = field;
   }

   @Override
   public String getField() {
      return this.field;
   }

   public void setField(String field) {
      this.field = field;

      // optional
      super.setField(field);
   }
}

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

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

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

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

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

Kotlin support

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

Kotlin object creation

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

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

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

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

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

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

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

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

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

考虑以下 dataPerson

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

上述类编译为具有显式构造函数的典型类。我们可以通过添加另一个构造函数并用 @PersistenceCreator 注释它来定制此类,以指示构造函数偏好:

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

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

Kotlin 支持参数可选择性,允许在不提供参数时使用默认值。当 Spring Data 检测到带有参数默认值的构造函数时,它会让这些参数保持缺失状态(或仅返回 null),以便 Kotlin 可以应用参数默认值。考虑以下对 name 应用参数默认值的类

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

每次当 name 参数既不属于结果的一部分或其值为 null,则 name 将默认为 unknown

Property population of Kotlin data classes

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

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

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

Kotlin Overriding Properties

Kotlin 允许声明 `` 来更改子类中的属性。

open class SuperType(open var field: Int)

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

这种安排使两个名称为 field 的属性得以呈现。Kotlin 中为每个类中每个属性生成了属性访问器(getter 和 setter)。实际上,该代码如下所示:

public class SuperType {

   private int field;

   public SuperType(int field) {
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

public final class SubType extends SuperType {

   private int field;

   public SubType(int field) {
      super(field);
      this.field = field;
   }

   public int getField() {
      return this.field;
   }

   public void setField(int field) {
      this.field = field;
   }
}

SubType 上的 getter 和 setter 仅设置 SubType.field 而不设置 SuperType.field。在这样的安排中,使用构造函数是设置 SuperType.field 的唯一默认方法。向 SubType 中添加一个方法以通过 this.SuperType.field = … 来设置 SuperType.field 是可能的,但是不属于受支持的约定。属性覆盖在某种程度上创建了冲突,因为这些属性共享相同名称,但可能表示两个不同的值。我们通常建议使用不同的属性名称。

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

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

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

  3. 由于不能设置超属性,所以不能使用 @AccessType(PROPERTY)

Kotlin Value Classes

Kotlin 值类被设计用于更具表现力的领域模型来使基础概念明确化。Spring Data 可以读取和写入使用值类定义属性的类型。

考虑以下领域模型:

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

data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) 2
1 具有非空值类型的简单值类。
2 数据类使用 EmailAddress 值类定义属性。

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

Documents and Fields

所有实体都应该用 @Document 注释进行注释,但它不是必需的。

此外,实体中的每个字段都应该用 @Field 注释进行注释。虽然从严格意义上来说这是可选的,但是它有助于减少极端情况并明确显示实体的意图和设计。它还可以用于在不同的名称下存储字段。

还有一个特殊的 @Id 注释,它需要始终存在。最佳实践是还将属性命名为 id

这是一个非常简单的 User 实体:

Example 1. A simple Document with Fields
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 值,并且不能混合这两种方法。

如果你希望文档中的字段名称和实体中使用的字段名称有不同的表示,可以在 @Field 注释中设置不同的名称。例如,如果你希望让你的文档保持小巧,可以将 firstname 字段设置为 @Field("fname")。在 JSON 文档中,你将看到 {"fname": ".."} 而不是 {"firstname": ".."}

@Id 注释必须存在,因为 Couchbase 中的每个文档都需要一个唯一键。该键必须是任何具有最大 250 个字符长度的字符串。随意使用任何适合你的用例的键,无论是 UUID、电子邮件地址还是其他任何内容。

对 Couchbase Server 存储桶的写入可以随意分配持久性要求;它指示 Couchbase Server 在集群中的多个节点的内存和/或磁盘位置更新指定文档,然后考虑提交写入。默认的持久性要求也可以通过 @Document@Durability 注释进行配置。例如: @Document(durabilityLevel = DurabilityLevel.MAJORITY) 将强制将变更复制到大多数数据服务节点。这两个注释都支持通过 durabilityExpression 属性(注意 SPEL 不受支持)基于表达式的持久性级别分配。

Datatypes and Converters

首选的存储格式是 JSON。它很好,但与许多数据表示一样,它允许的数据类型少于你可以在 Java 中直接表示的数据类型。因此,对于所有非基本类型,都需要以某种形式进行转换到受支持的类型和从受支持的类型转换。

对于以下实体字段类型,不需要添加特殊处理:

Table 1. Primitive Types
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”)和列表,所以可以自然地转换 MapList 类型。如果它们仅包含上段落中的基本字段类型,则不需要添加特殊处理。以下是一个示例:

Example 2. A Document with Map and 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 表示可能如下所示:

Example 3. A Document with Map and List - JSON
{
    "_class": "foo.User",
    "childrenAges": {
        "Alice": 10,
        "Bob": 5
    },
    "firstnames": [
        "Foo",
        "Bar",
        "Baz"
    ]
}

你不必每次都将所有内容细分为基本类型和列表/映射。当然,你还可以从这些基本值中组合出其他对象。让我们修改最后一个示例,以便我们想要存储一个 ChildrenList

Example 4. A Document with composed objects
@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;
        }

    }

}

一个填充后的对象可能如下所示:

Example 5. A Document with composed objects - JSON
{
  "_class": "foo.User",
  "children": [
    {
      "age": 4,
      "name": "Alice"
    },
    {
      "age": 3,
      "name": "Bob"
    }
  ],
  "firstnames": [
    "Foo",
    "Bar",
    "Baz"
  ]
}

大多数情况下,你还需要存储诸如 Date 之类的临时值。由于它不能直接存储在 JSON 中,因此需要进行转换。该库为 DateCalendar 和 JodaTime 类型(如果在类路径中)实现了默认转换器。所有这些在文档中默认用 UNIX 时间戳(数字)表示。你可以使用后面显示的自定义转换器随时覆盖默认行为。以下是一个示例:

Example 6. A Document with Date and Calendar
@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;
    }

}

一个填充后的对象可能如下所示:

Example 7. A Document with Date and Calendar - JSON
{
  "title": "a blog post title",
  "_class": "foo.BlogPost",
  "updated": 1394610843,
  "created": 1394610843897
}

可以选择通过将系统属性 org.springframework.data.couchbase.useISOStringConverterForDate 设置为 true,将日期转换到 ISO-8601 兼容的字符串或反之。如果你想覆盖转换器或实现你自己的转换器,这也是可能的。该库实现了通用的 Spring 转换器模式。你可以在配置中在 bean 创建时插入自定义转换器。以下是如何配置它(在被覆盖的 AbstractCouchbaseConfiguration 中):

Example 8. Custom Converters
@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 */;
    }

}

在使用自定义转换时需要注意一些事项:

  • 为确保明确无歧义,始终在您的转换器中使用 @WritingConverter@ReadingConverter 注释。如果您在处理原始类型转换,这样做将有助于减少可能出现的错误转换。

  • 如果您实现了一个写转换器,确保只解码为原始类型、地图和列表。如果您需要更复杂的的对象类型,请使用 underlying translation 引擎也理解的 CouchbaseDocumentCouchbaseList 类型。您最合适的做法是坚守尽可能简单的转换。

  • 始终将更特殊的转换器置于通用转换器之前,以避免出现错误执行转换器的可能。

  • 对于日期,读取转换器应该能够从任何 Number(而不只是 Long)读取。这是 N1QL 支持所必需的。

Optimistic Locking

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

乐观并发往往比悲观并发或事务提供更好的性能,因为不会真正锁定数据,并且不会存储有关操作的额外信息(没有事务日志)。

为了实现乐观锁,Couchbase 使用 CAS(比较并交换)方法。当一个文档发生突变时,CAS 值也会改变。CAS 对客户端是不可见的,唯一需要知道的是,当内容或元信息发生更改时它也会更改。

在其他数据存储中,可以通过递增计数器的任意版本字段实现类似的行为。由于 Couchbase 以一种更好的方式支持此操作,因此很容易实现。如果你想要自动乐观锁支持,你需要做的就是给一个长字段添加一个 @Version 注解,如下所示:

Example 9. A Document with optimistic locking.
@Document
public class User {

        @Version
        private long version;

        // constructor, getters, setters...
}

如果你通过模板或存储库加载一个文档,版本字段将自动填充为当前 CAS 值。重要的是要注意,你不应访问该字段或自行更改它。一旦你将文档保存回去,它将成功或失败,并引发 OptimisticLockingFailureException。如果你遇到这样的异常,则进一步的方法取决于你希望应用实现什么。你应该重试整个加载-更新-写入周期或将错误传播到上层以进行适当处理。

Validation

该库支持 JSR 303 验证,它基于实体中的直接注解。当然,你可以在服务层中添加各种验证,但这种方法很好地耦合到了你的实际实体中。

要使其正常工作,你需要包含两个其他依赖项。JSR 303 和实现它的库,例如 Hibernate 支持的那个:

Example 10. Validation dependencies
<dependency>
  <groupId>javax.validation</groupId>
  <artifactId>validation-api</artifactId>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
</dependency>

现在你需要向配置添加两个 bean:

Example 11. Validation beans
@Bean
public LocalValidatorFactoryBean validator() {
    return new LocalValidatorFactoryBean();
}

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

现在你可以使用 JSR 303 注解为你的字段添加注解。如果 save() 上的验证失败,则会抛出 ConstraintViolationException

Example 12. Sample Validation Annotation
@Size(min = 10)
@Field
private String name;

Auditing

实体可以通过 Spring Data 审计机制自动审计(追踪哪个用户创建了对象、更新了对象以及在什么时候)。

首先,请注意,只有带有 @Version 注释字段的实体才能进行创建审计(否则框架会将创建解释为更新)。

审计通过用 @CreatedBy@CreatedDate@LastModifiedBy@LastModifiedDate 注释字段来工作。在持久化实体时,框架会自动向这些字段注入正确的值。xxxDate 注解必须放在 Date 字段(或兼容的 jodatime 类)上,而 xxxBy 注解可以放在任何类 T 的字段上(尽管两个字段必须是相同类型)。

要配置审计,首先你需要在上下文中有一个审计感知 bean。所述 bean 必须为 AuditorAware<T> 类型(允许生成一个值,该值可以存储在我们之前看到的类型 T 的 xxxBy 字段中)。其次,你必须通过使用 @EnableCouchbaseAuditing 注解在 @Configuration 类中激活审计。

这是一个示例:

Example 13. Sample Auditing Entity
@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

Example 14. Sample AuditorAware implementation
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 以及激活审核:

Example 15. Sample Auditing Configuration
@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();
    }