跳到主要内容

建模实体

ChatGPT-4o-mini 中英对照 Modeling Entities

本章描述了如何建模实体,并解释了它们在 Couchbase Server 中的对应表示。

对象映射基础

本节涵盖了 Spring Data 对象映射、对象创建、字段和属性访问、可变性和不可变性的基础知识。请注意,本节仅适用于不使用底层数据存储(如 JPA)对象映射的 Spring Data 模块。同时,请务必查阅特定于存储的部分,以获取特定于存储的对象映射,例如索引、自定义列或字段名称等。

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

  1. 通过使用暴露的构造函数创建实例。

  2. 实例填充以实现所有暴露的属性。

对象创建

Spring Data 会自动尝试检测持久化实体的构造函数,以用于实例化该类型的对象。解析算法的工作原理如下:

  1. 如果存在一个用 @PersistenceCreator 注解的静态工厂方法,则使用该方法。

  2. 如果存在一个构造函数,则使用该构造函数。

  3. 如果存在多个构造函数,并且恰好有一个用 @PersistenceCreator 注解,则使用该构造函数。

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

  5. 如果存在无参数构造函数,则使用该构造函数。其他构造函数将被忽略。

值解析假设构造函数/工厂方法的参数名称与实体的属性名称匹配,即解析将按照属性要被填充的方式进行,包括映射中的所有自定义(不同的数据存储列或字段名称等)。这还要求类文件中提供参数名称信息,或者构造函数上存在 @ConstructorProperties 注解。

值解析可以通过使用 Spring Framework 的 @Value 值注解,结合特定存储的 SpEL 表达式进行自定义。有关更多详细信息,请参阅特定存储映射的部分。

对象创建内部实现

为了避免反射的开销,Spring Data 对象创建默认使用一个在运行时生成的工厂类,该工厂类将直接调用领域类的构造函数。也就是说,对于这个示例类型:

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

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

class PersonObjectInstantiator implements ObjectInstantiator {

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

这给我们带来了大约 10% 的性能提升,相比于反射。为了使领域类符合这种优化,它需要遵循一组约束:

  • 它不能是一个私有类

  • 它不能是一个非静态内部类

  • 它不能是一个 CGLib 代理类

  • Spring Data 使用的构造函数不能是私有的

如果满足这些标准中的任何一个,Spring Data 将回退到通过反射进行实体实例化。

属性人口

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

  1. 如果属性是不可变的但暴露了 with… 方法(见下文),我们使用 with… 方法创建一个具有新属性值的新实体实例。

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

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

  4. 如果属性是不可变的,我们使用构造函数来进行持久化操作(见 对象创建)以创建实例的副本。

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

属性人口内部

类似于我们在 对象构造中的优化,我们还使用 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) {
// 字段赋值
}

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

void setLastname(String lastname) {
this.lastname = lastname;
}
}
java
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
}
}
}
java
  • PropertyAccessor 持有底层对象的可变实例。这是为了使不可变属性能够进行变更。

  • 默认情况下,Spring Data 使用字段访问来读取和写入属性值。根据 private 字段的可见性规则,使用 MethodHandles 与字段进行交互。

  • 该类公开了一个 withId(…) 方法,用于设置标识符,例如,当实例被插入到数据存储中并且生成了标识符时。调用 withId(…) 会创建一个新的 Person 对象。所有后续的变更将在新实例中进行,保留之前的实例不变。

  • 使用属性访问允许直接调用方法,而无需使用 MethodHandles

这使我们获得了约 25% 的性能提升,相较于反射。为了使域类符合此类优化,它需要遵循一组约束:

  • 类型不得位于默认包或 java 包下。

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

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

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

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

让我们来看看以下实体:

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;
}
}
java
  • 标识符属性是最终的,但在构造函数中设置为 null。该类公开了一个 withId(…) 方法,用于设置标识符,例如,当实例被插入到数据存储中并生成了标识符时。原始的 Person 实例保持不变,因为创建了一个新的实例。通常,对于其他由存储管理但可能需要在持久化操作中更改的属性,也会应用相同的模式。wither 方法是可选的,因为持久化构造函数(见 6)实际上是一个复制构造函数,设置属性将被转换为创建一个应用了新标识符值的新实例。

  • firstnamelastname 属性是普通的不可变属性,可能通过 getter 公开。

  • age 属性是一个不可变的,但从 birthday 属性派生的属性。根据所示设计,数据库值将优先于默认值,因为 Spring Data 使用唯一声明的构造函数。即使意图是优先考虑计算,也很重要的是这个构造函数也将 age 作为参数(以便可能忽略它),否则属性填充步骤将尝试设置 age 字段并因其不可变而失败,并且没有 with… 方法存在。

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

  • remarks 属性是可变的,通过调用 setter 方法来填充。

  • 该类公开了一个工厂方法和一个构造函数用于对象创建。这里的核心思想是使用工厂方法而不是额外的构造函数,以避免通过 @PersistenceCreator 进行构造函数消歧义的需要。相反,属性的默认值在工厂方法中处理。如果您希望 Spring Data 使用工厂方法进行对象实例化,请使用 @PersistenceCreator 注解它。

一般建议

  • 尽量使用不可变对象 — 不可变对象的创建非常简单,因为实例化一个对象只需调用其构造函数。这也避免了你的领域对象中充斥着允许客户端代码操作对象状态的 setter 方法。如果你需要这些方法,最好将它们设置为包保护,以便只能由有限数量的同位置类型调用。仅使用构造函数进行实例化的速度比属性赋值快多达 30%。

  • 提供一个全参数构造函数 — 即使你不能或不想将实体建模为不可变值,提供一个接受实体所有属性作为参数的构造函数(包括可变属性)仍然是有价值的,因为这允许对象映射跳过属性赋值以获得最佳性能。

  • 使用工厂方法而不是重载构造函数以避免 @PersistenceCreator — 为了获得最佳性能,通常需要一个全参数构造函数,我们通常希望暴露更多特定于应用程序用例的构造函数,省略诸如自动生成的标识符等内容。使用静态工厂方法来暴露这些全参数构造函数的变体是一种成熟的模式。

  • 确保遵循允许生成的实例化器和属性访问器类使用的约束 — 

  • 为了生成标识符,仍然使用一个 final 字段,结合全参数持久化构造函数(首选)或 with… 方法 — 

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

重写属性

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

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

Spring Data 模块通常支持重写属性以持有不同的值。从编程模型的角度来看,有几个需要考虑的事项:

  1. 哪个属性应该被持久化(默认为所有声明的属性)?你可以通过使用 @Transient 注解来排除某些属性。

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

  3. 使用 @AccessType(PROPERTY) 不能被使用,因为超属性通常不能在不对 setter 实现做出进一步假设的情况下被设置。

Kotlin 支持

Spring Data 适配了 Kotlin 的特性,以允许对象的创建和变更。

Kotlin 对象创建

Kotlin 类支持实例化,所有类默认是不可变的,并且需要显式的属性声明来定义可变属性。

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

  1. 如果存在一个带有 @PersistenceCreator 注解的构造函数,则使用该构造函数。

  2. 如果类型是一个 Kotlin 数据类,则使用主构造函数。

  3. 如果存在一个带有 @PersistenceCreator 注解的静态工厂方法,则使用该方法。

  4. 如果存在一个构造函数,则使用该构造函数。

  5. 如果存在多个构造函数,并且恰好有一个带有 @PersistenceCreator 注解,则使用该构造函数。

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

  7. 如果存在一个无参数构造函数,则使用该构造函数。其他构造函数将被忽略。

请提供 dataPerson 的代码,我将帮助你进行修改或提供相关信息。

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

上面的类编译为一个具有显式构造函数的典型类。我们可以通过添加另一个构造函数并用 @PersistenceCreator 注解来定制这个类,以指示构造函数的优先级:

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

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

Kotlin 通过允许使用默认值来支持参数的可选性,如果未提供参数。 当 Spring Data 检测到具有参数默认值的构造函数时,如果数据存储未提供值(或简单地返回 null),则会将这些参数留空,以便 Kotlin 可以应用参数默认值。 考虑以下应用了参数默认值的 name 的类。

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

每当 name 参数不在结果中或其值为 null 时,name 默认值为 unknown

备注

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

Kotlin 数据类的属性赋值

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

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

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

Kotlin 重写属性

Kotlin 允许声明 属性重写 以在子类中更改属性。

open class SuperType(open var field: Int)

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

这样的安排使得两个属性具有相同的名称 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;
}
}
java

SubType 上的获取器和设置器仅设置 SubType.field 而不设置 SuperType.field。在这种安排下,使用构造函数是设置 SuperType.field 的唯一默认方法。通过 this.SuperType.field = …SubType 中添加一个方法来设置 SuperType.field 是可能的,但这超出了支持的约定。属性重写在某种程度上会产生冲突,因为这些属性共享相同的名称,但可能代表两个不同的值。我们通常建议使用不同的属性名称。

Spring Data 模块通常支持重写属性以持有不同的值。从编程模型的角度来看,有几个需要考虑的事项:

  1. 哪些属性应该被持久化(默认为所有声明的属性)?你可以通过使用 @Transient 注解来排除某些属性。

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

  3. 使用 @AccessType(PROPERTY) 不能被使用,因为超属性不能被设置。

Kotlin 值类

Kotlin 值类旨在提供更具表现力的领域模型,以使底层概念变得明确。Spring Data 可以读取和写入使用值类定义属性的类型。

请提供您想要翻译的具体内容或问题,我将很乐意帮助您。

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

data class Contact(val id: String, val name:String, val emailAddress: EmailAddress) 2
kotlin
  • 一个简单的值类,具有非空值类型。

  • 数据类使用 EmailAddress 值类定义一个属性。

备注

使用非原始值类型的非空属性在编译后的类中被扁平化为值类型。可空的原始值类型或可空的值内值类型则使用其包装类型表示,这会影响值类型在数据库中的表示方式。

文档和字段

所有实体都应该使用 @Document 注解进行标注,但这不是强制性的。

另外,实体中的每个字段都应该使用 @Field 注解。严格来说,这个注解是可选的,但它有助于减少边界情况,并清晰地展示实体的意图和设计。它还可以用来将字段存储为不同的名称。

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

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

示例 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;
}
}
java

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 服务器桶的操作可以选择性地分配耐久性要求;这指示 Couchbase 服务器在集群中的多个节点的内存和/或磁盘位置上更新指定的文档,然后才将写入视为已提交。默认的耐久性要求也可以通过 @Document@Durability 注解进行配置。例如:@Document(durabilityLevel = DurabilityLevel.MAJORITY) 将强制将变更复制到大多数数据服务节点。这两个注解都支持通过 durabilityExpression 属性进行基于表达式的耐久性级别分配(注意不支持 SPEL)。

数据类型和转换器

首选的存储格式是 JSON。它很好,但像许多数据表示一样,它允许的数据类型少于你可以直接在 Java 中表达的类型。因此,对于所有非原始类型,需要进行某种形式的转换,以便与支持的类型进行相互转换。

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

表 1. 原始类型

Java 类型JSON 表示
字符串字符串
布尔值布尔值
字节数字
短整型数字
整型数字
长整型数字
浮点型数字
双精度浮点型数字
空值写入时被忽略

由于 JSON 支持对象(“映射”)和列表,因此 MapList 类型可以自然地转换。如果它们仅包含上段提到的基本字段类型,则不需要添加特殊处理。以下是一个示例:

示例 2. 带有地图和列表的文档

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

}
java

存储一个用户及其一些示例数据可以看起来像这样,作为 JSON 表示:

示例 3. 带有地图和列表的文档 - JSON

{
"_class": "foo.User",
"childrenAges": {
"Alice": 10,
"Bob": 5
},
"firstnames": [
"Foo",
"Bar",
"Baz"
]
}
json

你不需要总是将所有东西拆解成基本类型和列表/映射。当然,你也可以用这些基本值组成其他对象。让我们修改上一个例子,假设我们要存储一个 ChildrenList

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

}

}
java

一个填充的对象可以看起来像:

示例 5. 一个包含复合对象的文档 - JSON

{
"_class": "foo.User",
"children": [
{
"age": 4,
"name": "Alice"
},
{
"age": 3,
"name": "Bob"
}
],
"firstnames": [
"Foo",
"Bar",
"Baz"
]
}
json

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

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

}
java

一个已填充的对象可以看起来像:

示例 7. 带有日期和日历的文档 - JSON

{
"title": "a blog post title",
"_class": "foo.BlogPost",
"updated": 1394610843,
"created": 1394610843897
}
json

可选地,可以通过将系统属性 org.springframework.data.couchbase.useISOStringConverterForDate 设置为 true 来将 Date 转换为符合 ISO-8601 的字符串,反之亦然。如果您想要覆盖一个转换器或实现自己的转换器,这也是可能的。该库实现了一般的 Spring 转换器模式。您可以在配置中在 bean 创建时插入自定义转换器。以下是您如何配置它(在您重写的 AbstractCouchbaseConfiguration 中):

示例 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 */;
}

}
java

关于自定义转换,有几点需要注意:

  • 为了消除歧义,始终在您的转换器上使用 @WritingConverter@ReadingConverter 注解。特别是当您处理基本类型转换时,这将有助于减少可能的错误转换。

  • 如果您实现了写入转换器,请确保仅解码为基本类型、映射和列表。如果您需要更复杂的对象类型,请使用 CouchbaseDocumentCouchbaseList 类型,这些类型也被底层翻译引擎理解。您最好的选择是尽可能简单的转换。

  • 始终将更特殊的转换器放在通用转换器之前,以避免错误的转换器被执行。

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

乐观锁

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

乐观并发通常比悲观并发或事务提供更好的性能,因为数据上没有实际的锁定,也没有关于操作的额外信息被存储(没有事务日志)。

为了实现乐观锁定,Couchbase 使用 CAS(比较和交换)方法。当文档被修改时,CAS 值也会发生变化。CAS 对客户端是不可见的,您只需知道,当内容或元信息发生变化时,它也会变化。

在其他数据存储中,可以通过一个递增计数的任意版本字段实现类似的行为。由于 Couchbase 以更好的方式支持这一点,因此实现起来很简单。如果您想要自动乐观锁支持,您只需在一个长整型字段上添加 @Version 注解,如下所示:

示例 9. 一个带有乐观锁定的文档。

@Document
public class User {

@Version
private long version;

// constructor, getters, setters...
}
java

如果您通过模板或存储库加载文档,版本字段将自动填充当前的 CAS 值。重要的是要注意,您不应该访问该字段,甚至不应该自行更改它。一旦您将文档保存回去,它将成功或失败,并抛出 OptimisticLockingFailureException。如果您遇到这样的异常,进一步的处理取决于您希望在应用程序中实现的目标。您应该重新尝试完整的加载-更新-写入周期,或者将错误传播到上层以进行适当处理。

验证

该库支持 JSR 303 验证,基于直接在实体中的注解。当然,您可以在服务层添加各种验证,但这种方式与您的实际实体紧密耦合。

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

示例 10. 验证依赖关系

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
xml
// ... existing code ...
{{ add_bean_1 }}
// ... existing code ...
{{ add_bean_2 }}
// ... existing code ...
language:path/to/configuration

Explanation of Updates:

  • Added two new beans to the configuration. Please specify the details of the beans you want to add for a more precise code update.

示例 11. 验证 Bean

@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}

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

现在你可以使用 JSR303 注解来标注你的字段。如果在 save() 方法中验证失败,将抛出 ConstraintViolationException

示例 12. 示例验证注释

@Size(min = 10)
@Field
private String name;
java

审计

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

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

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

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

请提供您想要翻译的内容,我将为您进行翻译。

示例 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/...
}
java

注意 @CreatedBy@LastModifiedBy 都放在一个 String 字段上,因此我们的 AuditorAware 必须与 String 一起工作。

示例 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

为了将所有内容结合在一起,我们使用 Java 配置来声明一个 AuditorAware bean 并激活审计:

示例 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();
}
java