对象映射

MappingMongoConverter 提供了丰富的映射支持。该转换器包含一个元数据模型,提供了一整套功能,用于将领域对象映射到 MongoDB 文档。映射元数据模型通过使用领域对象上的注释来填充。但是,该基础设施并不仅限于使用注释作为元数据信息唯一的来源。MappingMongoConverter 还允许您将对象映射到文档,而无需提供任何其他元数据,方法是遵循一组约定。

本节介绍 MappingMongoConverter 的功能,包括基础知识、如何使用约定将对象映射到文档以及如何使用基于注释的映射元数据覆盖这些约定。

对象映射基础知识

本节涵盖 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) { … }
}

我们将在运行时创建一个语义等效于此工厂类的工厂类

class PersonObjectInstantiator implements ObjectInstantiator {

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

这使我们比反射获得了大约 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) {
    // 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 拥有底层对象的 mutable 实例。这是为了启用对其他不可变属性的修改。
2 默认情况下,Spring Data 使用字段访问来读取和写入属性值。根据private字段的可见性规则,MethodHandles用于与字段交互。
3 该类公开了一个withId(…)方法,用于设置标识符,例如当一个实例被插入到数据存储中并且已经生成了一个标识符时。调用withId(…)创建一个新的Person对象。所有后续修改都将在新实例中进行,而不会影响之前的实例。
4 使用属性访问允许直接调用方法,而无需使用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;
  }
}
1 标识符属性是最终的,但在构造函数中设置为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 方法。如果您需要这些方法,最好将它们设为包保护,以便只能由有限数量的同位类型调用它们。仅构造函数实现比属性填充快 30%。

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

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

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

  • 对于要生成的标识符,仍然使用最终字段与 all-arguments 持久性构造函数(首选)或 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);
   }
}

这两个类都使用可赋值类型定义了一个 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)

上面的类编译为具有显式构造函数的典型类。我们可以通过添加另一个构造函数并用 @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

Kotlin 数据类的属性填充

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

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

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

Kotlin 重写属性

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 值类

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 值类定义属性的数据类。
使用非原始值类型的非空属性在已编译类中展平为值类型。可空原始值类型或可空值中值类型使用其包装类型表示,这会影响值类型在数据库中的表示方式。

基于约定的映射

当未提供其他映射元数据时,MappingMongoConverter 有一些用于将对象映射到文档的约定。这些约定是

  • Java 短类名按以下方式映射到集合名称。类 com.bigbank.SavingsAccount 映射到 savingsAccount 集合名称。

  • 所有嵌套对象都存储为文档中的嵌套对象,而不是 DBRef。

  • 转换器使用向其注册的任何 Spring 转换器来覆盖对象属性到文档字段和值的默认映射。

  • 对象的字段用于转换到文档中的字段,反之亦然。不使用公共 JavaBean 属性。

  • 如果您有一个单一的非零参数构造函数,其构造函数参数名称与文档的顶级字段名称匹配,则使用该构造函数。否则,使用零参数构造函数。如果有多个非零参数构造函数,则会引发异常。

映射层中如何处理 _id 字段。

MongoDB 要求您为所有文档提供一个 _id 字段。如果您没有提供,则驱动程序将分配一个具有生成值的 ObjectId。_id 字段可以是除数组之外的任何类型,只要它是唯一的。驱动程序自然支持所有原始类型和日期。使用 MappingMongoConverter 时,有一些规则控制如何将 Java 类的属性映射到 _id 字段。

以下概述了将映射到 _id 文档字段的字段

  • 使用 @Idorg.springframework.data.annotation.Id)注释的字段将映射到 _id 字段。
    此外,文档字段的名称可以通过 @Field 注释进行自定义,在这种情况下,文档将不包含 _id 字段。

  • 没有注释但名为 id 的字段将映射到 _id 字段。

表 1. _id 字段定义转换示例
字段定义 MongoDB 中生成 Id 字段名

String id

_id

@Field String id

_id

@Field("x") String id

x

@Id String x

_id

@Field("x") @Id String y

_id(忽略 @Field(name)@Id 优先)

以下概述了对映射到 _id 文档字段的属性执行的类型转换(如果有)。

  • 如果在 Java 类中将名为 id 的字段声明为 String 或 BigInteger,则如果可能,它将被转换为 ObjectId 并存储为 ObjectId。ObjectId 作为字段类型也是有效的。如果在应用程序中为 id 指定值,则 MongoDB 驱动程序将执行转换为 ObjectId。如果指定的 id 值无法转换为 ObjectId,则该值将按原样存储在文档的 _id 字段中。如果该字段用 @Id 注释,这也适用。

  • 如果在 Java 类中使用 @MongoId 注释字段,则它将被转换为并存储为使用其实际类型。除非 @MongoId 声明了所需的字段类型,否则不会发生进一步的转换。如果未为 id 字段提供值,则将创建一个新的 ObjectId 并将其转换为属性类型。

  • 如果在 Java 类中使用 @MongoId(FieldType.…) 注释字段,则将尝试将值转换为声明的 FieldType。如果未为 id 字段提供值,则将创建一个新的 ObjectId 并将其转换为声明的类型。

  • 如果在 Java 类中未将名为 id 的字段声明为 String、BigInteger 或 ObjectID,则应在应用程序中为其分配一个值,以便可以按原样将其存储在文档的 _id 字段中。

  • 如果 Java 类中不存在名为 id 的字段,则驱动程序将生成一个隐式 _id 文件,但不将其映射到 Java 类的属性或字段。

在查询和更新时,MongoTemplate 将使用转换器来处理 QueryUpdate 对象的转换,这些对象对应于保存文档的上述规则,因此查询中使用的字段名称和类型将能够与域类中的内容匹配。

数据映射和类型转换

Spring Data MongoDB 支持所有可以表示为 BSON(MongoDB 的内部文档格式)的类型。除了这些类型之外,Spring Data MongoDB 还提供了一组内置转换器来映射其他类型。你可以提供自己的转换器来调整类型转换。有关更多详细信息,请参阅 自定义转换 - 覆盖默认映射

内置类型转换
表 2. 类型
类型 类型转换 示例

字符串

原生

{"firstname" : "Dave"}

double, Double, float, Float

原生

{"weight" : 42.5}

int, Integer, short, Short

原生
32 位整数

{"height" : 42}

long, Long

原生
64 位整数

{"height" : 42}

Date, Timestamp

原生

{"date" : ISODate("2019-11-12T23:00:00.809Z")}

byte[]

原生

{"bin" : { "$binary" : "AQIDBA==", "$type" : "00" }}

java.util.UUID(旧版 UUID)

原生

{"uuid" : { "$binary" : "MEaf1CFQ6lSphaa3b9AtlA==", "$type" : "03" }}

日期

原生

{"date" : ISODate("2019-11-12T23:00:00.809Z")}

ObjectId

原生

{"_id" : ObjectId("5707a2690364aba3136ab870")}

数组、ListBasicDBList

原生

{"cookies" : [ … ]}

boolean, Boolean

原生

{"active" : true}

null

原生

{"value" : null}

文档

原生

{"value" : { … }}

Decimal128

原生

{"value" : NumberDecimal(…)}

AtomicInteger
在实际转换前调用 get()

转换器
32 位整数

{"value" : "741" }

AtomicLong
在实际转换前调用 get()

转换器
64 位整数

{"value" : "741" }

BigInteger

转换器
字符串

{"value" : "741" }

BigDecimal

转换器
字符串

{"value" : "741.99" }

URL

转换器

{"website" : "https://springframework.org.cn/projects/spring-data-mongodb/" }

区域设置

转换器

{"locale : "en_US" }

char, Character

转换器

{"char" : "a" }

NamedMongoScript

转换器
代码

{"_id" : "script name", value: (some javascript code)}

java.util.Currency

转换器

{"currencyCode" : "EUR"}

Instant
(Java 8)

原生

{"date" : ISODate("2019-11-12T23:00:00.809Z")}

Instant
(Joda, JSR310-BackPort)

转换器

{"date" : ISODate("2019-11-12T23:00:00.809Z")}

LocalDate
(Joda, Java 8, JSR310-BackPort)

转换器/原生 (Java8)[1]

{"date" : ISODate("2019-11-12T00:00:00.000Z")}

LocalDateTime, LocalTime
(Joda, Java 8, JSR310-BackPort)

转换器/原生 (Java8)[2]

{"date" : ISODate("2019-11-12T23:00:00.809Z")}

DateTime (Joda)

转换器

{"date" : ISODate("2019-11-12T23:00:00.809Z")}

ZoneId (Java 8, JSR310-BackPort)

转换器

{"zoneId" : "ECT - Europe/Paris"}

Box

转换器

{"box" : { "first" : { "x" : 1.0 , "y" : 2.0} , "second" : { "x" : 3.0 , "y" : 4.0}}

Polygon

转换器

{"polygon" : { "points" : [ { "x" : 1.0 , "y" : 2.0} , { "x" : 3.0 , "y" : 4.0} , { "x" : 4.0 , "y" : 5.0}]}}

Circle

转换器

{"circle" : { "center" : { "x" : 1.0 , "y" : 2.0} , "radius" : 3.0 , "metric" : "NEUTRAL"}}

Point

转换器

{"point" : { "x" : 1.0 , "y" : 2.0}}

GeoJsonPoint

转换器

{"point" : { "type" : "Point" , "coordinates" : [3.0 , 4.0] }}

GeoJsonMultiPoint

转换器

{"geoJsonLineString" : {"type":"MultiPoint", "coordinates": [ [ 0 , 0 ], [ 0 , 1 ], [ 1 , 1 ] ] }}

球体

转换器

{"sphere" : { "center" : { "x" : 1.0 , "y" : 2.0} , "radius" : 3.0 , "metric" : "NEUTRAL"}}

GeoJsonPolygon

转换器

{"polygon" : { "type" : "Polygon", "coordinates" : [[ [ 0 , 0 ], [ 3 , 6 ], [ 6 , 1 ], [ 0 , 0 ] ]] }}

GeoJsonMultiPolygon

转换器

{"geoJsonMultiPolygon" : { "type" : "MultiPolygon", "coordinates" : [ [ [ [ -73.958 , 40.8003 ] , [ -73.9498 , 40.7968 ] ] ], [ [ [ -73.973 , 40.7648 ] , [ -73.9588 , 40.8003 ] ] ] ] }}

GeoJsonLineString

转换器

{ "geoJsonLineString" : { "type" : "LineString", "coordinates" : [ [ 40 , 5 ], [ 41 , 6 ] ] }}

GeoJsonMultiLineString

转换器

{"geoJsonLineString" : { "type" : "MultiLineString", coordinates: [ [ [ -73.97162 , 40.78205 ], [ -73.96374 , 40.77715 ] ], [ [ -73.97880 , 40.77247 ], [ -73.97036 , 40.76811 ] ] ] }}

集合处理

集合处理取决于 MongoDB 返回的实际值。

  • 如果文档包含映射到集合的字段,则映射不会更新属性。这意味着该值将保持为 null、java 默认值或对象创建期间设置的任何值。

  • 如果文档包含要映射的字段,但该字段包含 null 值(如:{ 'list' : null }),则属性值将设置为 null

  • 如果文档包含要映射到集合的字段,且该字段null(如:{ 'list' : [ …​ ] }),则集合将使用映射值填充。

通常,如果你使用构造函数创建,则可以获取要设置的值。如果查询响应未提供属性值,则属性填充可以使用默认初始化值。

映射配置

除非显式配置,否则在创建 `MongoTemplate` 时,默认情况下会创建一个 `MappingMongoConverter` 实例。您可以创建自己的 `MappingMongoConverter` 实例。这样做可以让您指定类路径中可以找到域类的具体位置,以便 Spring Data MongoDB 可以提取元数据并构建索引。此外,通过创建自己的实例,您可以注册 Spring 转换器以将特定类映射到数据库或从数据库映射。

您可以使用基于 Java 或基于 XML 的元数据来配置 `MappingMongoConverter` 以及 `com.mongodb.client.MongoClient` 和 MongoTemplate。以下示例显示配置

  • Java

  • XML

@Configuration
public class MongoConfig extends AbstractMongoClientConfiguration {

  @Override
  public String getDatabaseName() {
    return "database";
  }

  // the following are optional

  @Override
  public String getMappingBasePackage() { (1)
    return "com.bigbank.domain";
  }

  @Override
  void configureConverters(MongoConverterConfigurationAdapter adapter) { (2)

  	adapter.registerConverter(new org.springframework.data.mongodb.test.PersonReadConverter());
  	adapter.registerConverter(new org.springframework.data.mongodb.test.PersonWriteConverter());
  }

  @Bean
  public LoggingEventListener<MongoMappingEvent> mappingEventsListener() {
    return new LoggingEventListener<MongoMappingEvent>();
  }
}
1 映射基础包定义用于扫描实体的根路径,这些实体用于预初始化 `MappingContext`。默认情况下,使用配置类包。
2 为特定域类型配置其他自定义转换器,这些转换器使用您的自定义实现替换这些类型的默认映射过程。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:mongo="http://www.springframework.org/schema/data/mongo"
  xsi:schemaLocation="
    http://www.springframework.org/schema/data/mongo https://www.springframework.org/schema/data/mongo/spring-mongo.xsd
    http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <!-- Default bean name is 'mongo' -->
  <mongo:mongo-client host="localhost" port="27017"/>

  <mongo:db-factory dbname="database" mongo-ref="mongoClient"/>

  <!-- by default look for a Mongo object named 'mongo' - default name used for the converter is 'mappingConverter' -->
  <mongo:mapping-converter base-package="com.bigbank.domain">
    <mongo:custom-converters>
      <mongo:converter ref="readConverter"/>
      <mongo:converter>
        <bean class="org.springframework.data.mongodb.test.PersonWriteConverter"/>
      </mongo:converter>
    </mongo:custom-converters>
  </mongo:mapping-converter>

  <bean id="readConverter" class="org.springframework.data.mongodb.test.PersonReadConverter"/>

  <!-- set the mapping converter to be used by the MongoTemplate -->
  <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
    <constructor-arg name="mongoDbFactory" ref="mongoDbFactory"/>
    <constructor-arg name="mongoConverter" ref="mappingConverter"/>
  </bean>

  <bean class="org.springframework.data.mongodb.core.mapping.event.LoggingEventListener"/>

</beans>

`AbstractMongoClientConfiguration` 要求您实现定义 `com.mongodb.client.MongoClient` 的方法以及提供数据库名称。`AbstractMongoClientConfiguration` 还具有一个名为 `getMappingBasePackage(…)` 的方法,您可以覆盖该方法以告诉转换器在何处扫描用 `@Document` 注解注释的类。

您可以通过覆盖 `customConversionsConfiguration` 方法向转换器添加其他转换器。可以通过 `MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs()` 启用 MongoDB 的原生 JSR-310 支持。前一个示例中还显示了一个 `LoggingEventListener`,它记录发布到 Spring 的 `ApplicationContextEvent` 基础设施的 `MongoMappingEvent` 实例。

Java 时间类型

我们建议使用 MongoDB 的原生 JSR-310 支持,方法是通过 `MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs()`,如上所述,因为它使用基于 `UTC` 的方法。从 Spring Data Commons 继承的 `java.time` 类型的默认 JSR-310 支持使用本地机器时区作为参考,并且仅应用于向后兼容性。

AbstractMongoClientConfiguration 创建 MongoTemplate 实例,并以 mongoTemplate 名称向容器注册该实例。

base-package 属性告诉它在何处扫描使用 @org.springframework.data.mongodb.core.mapping.Document 注释进行注释的类。

如果您希望依靠 Spring Boot 引导 Data MongoDB,但仍然希望覆盖配置的某些方面,则可能希望公开该类型的 Bean。对于自定义转换,例如,您可以选择注册 MongoCustomConversions 类型的 Bean,该 Bean 将被 Boot 基础设施选取。要了解更多信息,请务必阅读 Spring Boot 参考文档

基于元数据的映射

要充分利用 Spring Data MongoDB 支持中的对象映射功能,您应该使用 @Document 注释为映射对象添加注释。尽管映射框架不需要此注释(您的 POJO 映射正确,即使没有任何注释),但它可以让类路径扫描程序查找和预处理您的域对象以提取必要的元数据。如果您不使用此注释,则您的应用程序在您第一次存储域对象时会受到轻微的性能影响,因为映射框架需要构建其内部元数据模型,以便了解您的域对象的属性以及如何持久化它们。以下示例显示了一个域对象

示例 1. 示例域对象
package com.mycompany.domain;

@Document
public class Person {

  @Id
  private ObjectId id;

  @Indexed
  private Integer ssn;

  private String firstName;

  @Indexed
  private String lastName;
}
@Id 注释告诉映射器您希望将哪个属性用于 MongoDB _id 属性,而 @Indexed 注释告诉映射框架在文档的该属性上调用 createIndex(…),从而使搜索速度更快。自动索引创建仅对使用 @Document 进行注释的类型执行。
默认情况下,自动索引创建处于 禁用 状态,需要通过配置启用(请参阅 索引创建)。

映射注释概述

MappingMongoConverter 可以使用元数据来驱动对象到文档的映射。可以使用以下注释

  • @Id:应用于字段级别,以标记用于身份目的的字段。

  • @MongoId:应用于字段级别,以标记用于身份目的的字段。接受一个可选的 FieldType 来定制 id 转换。

  • @Document:应用于类级别,以指示此类是映射到数据库的候选类。您可以指定将存储数据的集合的名称。

  • @DBRef:应用于字段,以指示使用 com.mongodb.DBRef 存储该字段。

  • @DocumentReference:应用于字段,以指示将该字段存储为指向另一个文档的指针。这可以是单个值(默认情况下为 id),或通过转换器提供的 Document

  • @Indexed:应用于字段级别,以描述如何对字段建立索引。

  • @CompoundIndex(可重复):应用于类型级别,以声明复合索引。

  • @GeoSpatialIndexed:应用于字段级别,以描述如何对字段进行地理索引。

  • @TextIndexed:应用于字段级别,以标记要包含在文本索引中的字段。

  • @HashIndexed:应用于字段级别,以便在哈希索引中使用,以在分片集群中对数据进行分区。

  • @Language:应用于字段级别,以设置文本索引的语言覆盖属性。

  • @Transient:默认情况下,所有字段都映射到文档。此注释将应用它的字段排除在存储在数据库中。瞬态属性不能在持久性构造函数中使用,因为转换器无法为构造函数参数实现值。

  • @PersistenceConstructor:标记给定的构造函数(甚至是包保护的构造函数),以便在从数据库实例化对象时使用。构造函数参数按名称映射到检索到的 Document 中的键值。

  • @Value:此注释是 Spring Framework 的一部分。在映射框架中,它可以应用于构造函数参数。这使您可以使用 Spring 表达式语言语句来转换在数据库中检索到的键值,然后再将其用于构造域对象。为了引用给定文档的属性,必须使用诸如:@Value("#root.myProperty") 的表达式,其中 root 指示给定文档的根。

  • @Field:应用于字段级别,它允许描述字段的名称和类型,因为该字段将表示在 MongoDB BSON 文档中,从而允许名称和类型不同于类的字段名以及属性类型。

  • @Version:应用于字段级别,用于乐观锁定并在保存操作中检查修改。初始值为 zero(对于基本类型为 one),它将在每次更新时自动增加。

映射元数据基础设施在独立的 spring-data-commons 项目中定义,该项目与技术无关。特定的子类在 MongoDB 支持中用于支持基于注释的元数据。如果需求存在,还可以实施其他策略。

下面是更复杂的映射示例
@Document
@CompoundIndex(name = "age_idx", def = "{'lastName': 1, 'age': -1}")
public class Person<T extends Address> {

  @Id
  private String id;

  @Indexed(unique = true)
  private Integer ssn;

  @Field("fName")
  private String firstName;

  @Indexed
  private String lastName;

  private Integer age;

  @Transient
  private Integer accountTotal;

  @DBRef
  private List<Account> accounts;

  private T address;

  public Person(Integer ssn) {
    this.ssn = ssn;
  }

  @PersistenceConstructor
  public Person(Integer ssn, String firstName, String lastName, Integer age, T address) {
    this.ssn = ssn;
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.address = address;
  }

  public String getId() {
    return id;
  }

  // no setter for Id.  (getter is only exposed for some unit testing)

  public Integer getSsn() {
    return ssn;
  }

// other getters/setters omitted
}

当映射基础设施推断的本机 MongoDB 类型与预期类型不匹配时,@Field(targetType=…​) 可能派上用场。例如,对于 BigDecimal,它表示为 String 而不是 Decimal128,仅仅是因为早期版本的 MongoDB 服务器不支持它。

public class Balance {

  @Field(targetType = DECIMAL128)
  private BigDecimal value;

  // ...
}

您甚至可以考虑自己的自定义注释。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Field(targetType = FieldType.DECIMAL128)
public @interface Decimal128 { }

// ...

public class Balance {

  @Decimal128
  private BigDecimal value;

  // ...
}

特殊字段名称

一般来说,MongoDB 使用点 (.) 字符作为嵌套文档或数组的路径分隔符。这意味着在查询(或更新语句)中,像 a.b.c 这样的键针对如下所示的对象结构

{
    'a' : {
        'b' : {
            'c' : …
        }
    }
}

因此,在 MongoDB 5.0 之前,字段名称不得包含点 (.)。
使用 MappingMongoConverter#setMapKeyDotReplacement 可以通过用另一个字符替换写入时的点来规避存储 Map 结构时的一些限制。

converter.setMapKeyDotReplacement("-");
// ...

source.map = Map.of("key.with.dot", "value")
converter.write(source,...) // -> map : { 'key-with-dot', 'value' }

随着 MongoDB 5.0 的发布,对包含特殊字符的 Document 字段名称的限制被取消。我们强烈建议在 MongoDB 参考 中阅读更多有关在字段名称中使用点的限制。
要允许在 Map 结构中使用点,请在 MappingMongoConverter 上设置 preserveMapKeys

使用 @Field 可以通过两种方式自定义字段名称以考虑点。

  1. @Field(name = "a.b"):名称被视为路径。操作期望嵌套对象的结构,例如 { a : { b : … } }

  2. @Field(name = "a.b", fieldNameType = KEY):名称被视为原样名称。操作期望一个字段,其给定值为 { 'a.b' : ….. }

由于点字符在 MongoDB 查询和更新语句中的特殊性质,包含点的字段名称无法直接定位,因此被排除在派生查询方法中使用。考虑以下具有映射到名为 cat.id 的字段的 categoryId 属性的 Item

public class Item {

	@Field(name = "cat.id", fieldNameType = KEY)
	String categoryId;

	// ...
}

它的原始表示形式如下所示

{
    'cat.id' : "5b28b5e7-52c2",
    ...
}

由于我们无法直接定位 cat.id 字段(因为这将被解释为路径),我们需要 聚合框架 的帮助。

查询名称中带点的字段
template.query(Item.class)
    // $expr : { $eq : [ { $getField : { input : '$$CURRENT', 'cat.id' }, '5b28b5e7-52c2' ] }
    .matching(expr(ComparisonOperators.valueOf(ObjectOperators.getValueOf("value")).equalToValue("5b28b5e7-52c2"))) (1)
    .all();
1 映射层负责将属性名称 value 转换为实际字段名称。在此处使用目标字段名称也是绝对有效的。
更新名称中带点的字段
template.update(Item.class)
    .matching(where("id").is("r2d2"))
    // $replaceWith: { $setField : { input: '$$CURRENT', field : 'cat.id', value : 'af29-f87f4e933f97' } }
    .apply(AggregationUpdate.newUpdate(ReplaceWithOperation.replaceWithValue(ObjectOperators.setValueTo("value", "af29-f87f4e933f97")))) (1)
    .first();
1 映射层负责将属性名称 value 转换为实际字段名称。在此处使用目标字段名称也是绝对有效的。

上面显示了一个简单示例,其中特殊字段出现在顶级文档级别。嵌套级别越高,与该字段交互所需的聚合表达式的复杂性就越高。

自定义对象构建

映射子系统允许通过使用 @PersistenceConstructor 注解对构造函数进行注释来自定义对象构建。用于构造函数参数的值按以下方式解析

  • 如果某个参数使用 @Value 注解进行注释,则计算给定的表达式,并将结果用作参数值。

  • 如果 Java 类型具有一个属性,其名称与输入文档的给定字段匹配,则使用其属性信息来选择适当的构造函数参数,以将输入字段值传递给该参数。这仅在 Java .class 文件中存在参数名称信息时才有效,可以通过使用调试信息编译源代码或在 Java 8 中使用 javac 的新 -parameters 命令行开关来实现这一点。

  • 否则,将抛出 MappingException,指示无法绑定给定的构造函数参数。

class OrderItem {

  private @Id String id;
  private int quantity;
  private double unitPrice;

  OrderItem(String id, @Value("#root.qty ?: 0") int quantity, double unitPrice) {
    this.id = id;
    this.quantity = quantity;
    this.unitPrice = unitPrice;
  }

  // getters/setters ommitted
}

Document input = new Document("id", "4711");
input.put("unitPrice", 2.5);
input.put("qty",5);
OrderItem item = converter.read(OrderItem.class, input);
如果无法解析给定的属性路径,则 quantity 参数的 @Value 注解中的 SpEL 表达式将回退到值 0

可以在 MappingMongoConverterUnitTests 测试套件中找到有关如何使用 @PersistenceConstructor 注解的其他示例。

映射框架事件

在映射过程的生命周期中会触发事件。这在 生命周期事件 部分中进行了描述。

在 Spring ApplicationContext 中声明这些 Bean 会导致在分派事件时调用这些 Bean。


1. 使用 UTC 时区偏移。通过 MongoConverterConfigurationAdapter 进行配置
2. 使用 UTC 时区偏移。通过 MongoConverterConfigurationAdapter 进行配置