Object Mapping

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

MappingMongoConverter 提供了丰富的映射支持。转换器包含一个元数据模型,该元数据模型提供了一个完整的功能集,可将域对象映射到 MongoDB 文档。通过在域对象上使用注释填充映射元数据模型。然而,该基础架构并不仅限于使用注释作为元数据信息的唯一来源。MappingMongoConverter 还允许在不提供任何附加元数据的情况下将对象映射到文档,方法是遵循一系列约定。 本节介绍 MappingMongoConverter 的特性,包括基础知识、如何使用约定将对象映射到文档,以及如何使用基于注释的映射元数据覆盖这些约定。

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 值类定义属性。

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

Convention-based Mapping

当没有提供其他映射元数据时,MappingMongoConverter 有几个映射对象到文档的约定。约定是:

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

  • 所有嵌套对象都将作为嵌套对象存储在文档中,而 not 则存储为 DBRef。

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

  • 对象的字段用于转换到文档中的字段以及从文档中的字段转换回来。不会使用公共 JavaBean 属性。

  • 如果你只有一个具有非零个参数的构造器,且该构造器参数名称与文档的顶级字段名称匹配,则将使用该构造器。否则,将使用零个参数的构造器。如果有多个非零个参数的构造器,则会引发异常。

How the _id field is handled in the mapping layer.

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

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

  • 使用 @Id (org.springframework.data.annotation.Id) 作注释的字段将映射到 _id 字段。

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

  • 标识符的默认字段名称为 _id,可以通过 @Field 注释进行自定义。

Table 1. Examples for the translation of _id field definitions
Field definition Resulting Id-Fieldname in MongoDB

String id

_id

@Field String id

_id

@Field("x") String id

x

@Id String x

_id

@Field("x") @Id String x

_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 并将其转换为声明的类型。

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

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

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

Data Mapping and Type Conversion

Spring Data MongoDB 支持所有可以表示为 BSON(MongoDB 的内部文件格式)的类型。除了这些类型,Spring Data MongoDB 还提供了一组内置转换程序来映射其他类型。您可以提供您自己的转换程序来调整类型转换。详情请参阅 Custom Conversions - Overriding Default Mapping

Example 1. Built in Type conversions:
Table 2. Type
Type Type conversion Sample

String

native

{"firstname" : "Dave"}

double, Double, float, Float

native

{"weight" : 42.5}

int, Integer, short, Short

native 32-bit integer

{"height" : 42}

long, Long

native 64-bit integer

{"height" : 42}

Date, Timestamp

native

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

byte[]

native

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

java.util.UUID (Legacy UUID)

native

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

Date

native

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

ObjectId

native

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

Array, List, BasicDBList

native

{"cookies" : [ … ]}

boolean, Boolean

native

{"active" : true}

null

native

{"value" : null}

Document

native

{"value" : { … }}

Decimal128

native

{"value" : NumberDecimal(…)}

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

converter 32-bit integer

{"value" : "741" }

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

converter 64-bit integer

{"value" : "741" }

BigInteger

converter String

{"value" : "741" }

BigDecimal

converter String

{"value" : "741.99" }

URL

converter

{"website" : "https://spring.io/projects/spring-data-mongodb/" }

Locale

converter

{"locale : "en_US" }

char, Character

converter

{"char" : "a" }

NamedMongoScript

converter Code

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

java.util.Currency

converter

{"currencyCode" : "EUR"}

Instant (Java 8)

native

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

Instant (Joda, JSR310-BackPort)

converter

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

LocalDate (Joda, Java 8, JSR310-BackPort)

转换器/本机 (Java8) 使用 UTC 时区偏移。通过 MongoConverterConfigurationAdapter配置

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

LocalDateTimeLocalTime(Joda, Java8, JSR310-BackPort)

转换器/本机 (Java8) 使用 UTC 时区偏移。通过 MongoConverterConfigurationAdapter配置

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

DateTime (Joda)

converter

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

ZoneId (Java 8, JSR310-BackPort)

converter

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

Box

converter

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

Polygon

converter

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

Circle

converter

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

Point

converter

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

GeoJsonPoint

converter

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

GeoJsonMultiPoint

converter

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

Sphere

converter

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

GeoJsonPolygon

converter

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

GeoJsonMultiPolygon

converter

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

GeoJsonLineString

converter

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

GeoJsonMultiLineString

converter

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

Collection Handling

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

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

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

  • 如果某个文档包含一个要映射到 not null 集合的字段(例如: { 'list' : [ …​ ] }),那么该集合将填充映射的值。

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

Mapping Configuration

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

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

  • Java

@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
<?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 方法将其他转换器添加到转换器。MongoDB 的原生 JSR-310 支持可以通过 MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs() 启用。前一个示例中还展示了一个 LoggingEventListener,它记录发布到 Spring 的 ApplicationContextEvent 基础设施中的 MongoMappingEvent 实例。

Java Time Types

我们建议使用上面描述的 MongoConverterConfigurationAdapter.useNativeDriverJavaTimeCodecs() 方式使用 MongoDB 的原生 JSR-310 支持,因为它使用基于 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,它将被 Boot 基础设施选取。要详细了解此信息,请务必阅读 Spring Boot Reference Documentation

Metadata-based Mapping

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

Example 2. Example domain object
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 注释的类型执行自动索引创建。

默认情况下,自动索引创建是*disabled*,需要通过配置启用(请参阅Index Creation)。

Mapping Annotation Overview

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

  • @Id: 在字段级别应用以标记用于标识目的的字段。

  • @MongoId:在字段级别应用于标记用于识别目的的字段。接受可选 FieldType 以自定义 id 转换。

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

  • @DBRef:在字段处应用,以指示其将使用 com.mongodb.DBRef 存储。

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

  • @Indexed:在字段级别应用,以描述如何对该字段编制索引。

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

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

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

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

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

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

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

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

  • @Field: 应用于字段级别时,它可以描述字段的名称和类型(它将以该名称和类型表示在 MongoDB BSON 文档中),因此名称和类型可以不同于类的字段名和属性类型。

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

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

Example 3. Here is an example of a more complex mapping
@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 Server 的早期版本不支持它。

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;

  // ...
}

Special Field Names

一般而言,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 Reference 中有关使用字段名中的点的限制的更多信息。要在 Map 结构中允许使用点,请在 MappingMongoConverter 上设置 preserveMapKeys

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

  1. @Field(name = "a.b"): 名称被视为一个路径。操作需要使用 { a : { b : … } } 等嵌套对象的结构。

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

由于点号在 MongoDB 查询和更新声明中具有特殊性质,包含点号的字段名无法直接作为目标,因此被排除在派生查询方法中。考虑下面的 Item 具有一个映射到名为 cat.id 的字段的 categoryId 属性。

public class Item {

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

	// ...
}

它的原始表示将类似于

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

由于我们不能直接定位 `cat.id`字段(因为这会被解释成一条路径),所以我们需要 Aggregation Framework的帮助。

Query fields with a dot in its name
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 转换为实际的字段名称。在此处使用目标字段名称也完全有效。
Update fields with a dot in its name
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 转换为实际的字段名称。在此处使用目标字段名称也完全有效。

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

Customized Object Construction

映射子系统允许通过使用 @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);

@Value 注释中 quantity 参数中的 SpEL 表达式如果无法解析给定的属性路径,则会回退到值 0

可以在 MappingMongoConverterUnitTests 测试套件中找到使用 @PersistenceConstructor 注释的其他示例。

Mapping Framework Events

事件在映射过程的生命周期中触发。这在 Lifecycle Events 部分中有说明。

在 Spring ApplicationContext 中声明这些 Bean 会让它们在事件被分派时被调用。