此版本仍在开发中,尚未被视为稳定版。如需最新稳定版本,请使用 Spring Data MongoDB 5.0.4spring-doc.cadn.net.cn

解包类型

未包装的实体(Unwrapped entities)用于设计 Java 领域模型中的值对象,其属性会被展平到父级的 MongoDB 文档中。spring-doc.cadn.net.cn

解包类型映射

考虑以下领域模型,其中 User.name 被标注了 @Unwrapped 注解。 @Unwrapped 注解表明,UserName 的所有属性都应被展开(扁平化)到拥有 user 属性的 name 文档中。spring-doc.cadn.net.cn

示例 1. 解包对象的示例代码
class User {

    @Id
    String userId;

    @Unwrapped(onEmpty = USE_NULL) (1)
    UserName name;
}

class UserName {

    String firstname;

    String lastname;

}
{
  "_id" : "1da2ba06-3ba7",
  "firstname" : "Emma",
  "lastname" : "Frost"
}
1 加载 name 属性时,如果 nullfirstname 均为 lastname 或不存在,则其值将被设为 null。 通过使用 onEmpty=USE_EMPTY,将创建一个空的 UserName 对象,其属性可能为 null

对于更简洁的可嵌入类型声明,请使用 @Unwrapped.Nullable@Unwrapped.Empty,而不是 @Unwrapped(onEmpty = USE_NULL)@Unwrapped(onEmpty = USE_EMPTY)。 这两个注解都通过元注解方式标注了 JSR-305 的 @javax.annotation.Nonnull,以辅助进行空值检查。spring-doc.cadn.net.cn

可以在非包装对象中使用复杂类型。 然而,这些复杂类型本身不能是、也不能包含非包装字段。spring-doc.cadn.net.cn

解包类型的字段名称

通过使用 prefix 注解的可选 @Unwrapped 属性,可以对值对象进行多次展开(unwrap)。 这样做时,所选的前缀将被添加到被展开对象中的每个属性名或 @Field("…") 名称之前。 请注意,如果多个属性映射到同一个字段名,则它们的值会相互覆盖。spring-doc.cadn.net.cn

示例2. 带名称前缀的未包装对象的示例代码
class User {

    @Id
    String userId;

    @Unwrapped.Nullable(prefix = "u_") (1)
    UserName name;

    @Unwrapped.Nullable(prefix = "a_") (2)
    UserName name;
}

class UserName {

    String firstname;

    String lastname;
}
{
  "_id" : "a6a805bd-f95f",
  "u_firstname" : "Jean",             (1)
  "u_lastname" : "Grey",
  "a_firstname" : "Something",        (2)
  "a_lastname" : "Else"
}
1 UserName 的所有属性都以 u_ 为前缀。
2 UserName 的所有属性都以 a_ 为前缀。

在同一属性上同时使用 @Field 注解和 @Unwrapped 注解是没有意义的,因此会导致错误。 然而,在任意一个被展开(unwrapped)类型的属性上使用 @Field 注解则完全是有效的做法。spring-doc.cadn.net.cn

示例 3. 使用 @Field 注解解包对象的示例代码
public class User {

	@Id
    private String userId;

    @Unwrapped.Nullable(prefix = "u-") (1)
    UserName name;
}

public class UserName {

	@Field("first-name")              (2)
    private String firstname;

	@Field("last-name")
    private String lastname;
}
{
  "_id" : "2647f7b9-89da",
  "u-first-name" : "Barbara",         (2)
  "u-last-name" : "Gordon"
}
1 UserName 的所有属性都以 u- 为前缀。
2 最终的字段名称是通过连接 @Unwrapped(prefix)@Field(name) 得到的结果。

查询未包装对象

可以在类型级别和字段级别上定义针对未包装(unwrapped)属性的查询,因为所提供的 Criteria 会与领域类型进行匹配。 在生成实际查询时,会考虑前缀以及可能的自定义字段名称。 使用未包装对象的属性名称来匹配其包含的所有字段,如下方示例所示。spring-doc.cadn.net.cn

示例 4. 对未包装对象进行查询
UserName userName = new UserName("Carol", "Danvers")
Query findByUserName = query(where("name").is(userName));
User user = template.findOne(findByUserName, User.class);
db.collection.find({
  "firstname" : "Carol",
  "lastname" : "Danvers"
})

也可以直接使用展开对象的属性名称来访问其任意字段,如下方代码片段所示。spring-doc.cadn.net.cn

示例 5. 对未包装对象的字段进行查询
Query findByUserFirstName = query(where("name.firstname").is("Shuri"));
List<User> users = template.findAll(findByUserFirstName, User.class);
db.collection.find({
  "firstname" : "Shuri"
})

按未包装字段排序。

可以通过属性路径使用未包装对象的字段进行排序,如下例所示。spring-doc.cadn.net.cn

示例6. 对未包装字段进行排序
Query findByUserLastName = query(where("name.lastname").is("Romanoff"));
List<User> user = template.findAll(findByUserName.withSort(Sort.by("name.firstname")), User.class);
db.collection.find({
  "lastname" : "Romanoff"
}).sort({ "firstname" : 1 })

尽管可行,但直接使用未包装的对象本身作为排序条件会将其所有字段以不可预测的顺序包含在内,可能导致排序结果不准确。spring-doc.cadn.net.cn

对未解包对象的字段投影

未包装对象的字段可以整体进行投影,也可以像下面示例所示那样通过单个字段进行投影。spring-doc.cadn.net.cn

示例 7. 对未包装对象的项目。
Query findByUserLastName = query(where("name.firstname").is("Gamora"));
findByUserLastName.fields().include("name");                             (1)
List<User> user = template.findAll(findByUserName, User.class);
db.collection.find({
  "lastname" : "Gamora"
},
{
  "firstname" : 1,
  "lastname" : 1
})
1 对未包装对象的字段投影包含其所有属性。
示例8:对解包对象的字段进行投影。
Query findByUserLastName = query(where("name.lastname").is("Smoak"));
findByUserLastName.fields().include("name.firstname");                   (1)
List<User> user = template.findAll(findByUserName, User.class);
db.collection.find({
  "lastname" : "Smoak"
},
{
  "firstname" : 1
})
1 对未包装对象的字段投影包含其所有属性。

在未解包对象上通过示例查询。

未包装的对象可以在 Example 探针中像其他任何类型一样使用。 请参阅按示例查询部分,以了解更多关于此功能的信息。spring-doc.cadn.net.cn

在未包装对象上执行存储库查询。

Repository 抽象允许基于未包装对象的字段以及整个对象派生查询。spring-doc.cadn.net.cn

示例 9. 对未包装对象的仓库查询。
interface UserRepository extends CrudRepository<User, String> {

	List<User> findByName(UserName username);         (1)

	List<User> findByNameFirstname(String firstname); (2)
}
1 匹配解包对象的所有字段。
2 匹配 firstname

即使仓库的 create-query-indexes 命名空间属性设置为 true,对未包装对象的索引创建也会被暂停。spring-doc.cadn.net.cn

关于解包对象的更新

未包装的对象可以像领域模型中的任何其他对象一样进行更新。 映射层会负责将结构展平到其所在环境中。 既可以更新未包装对象的单个属性,也可以像下面示例中所示那样更新整个值。spring-doc.cadn.net.cn

示例 10. 更新一个未包装对象的单个字段。
Update update = new Update().set("name.firstname", "Janet");
template.update(User.class).matching(where("id").is("Wasp"))
   .apply(update).first()
db.collection.update({
  "_id" : "Wasp"
},
{
  "$set" { "firstname" : "Janet" }
},
{ ... }
)
示例 11. 更新一个未包装的对象。
Update update = new Update().set("name", new Name("Janet", "van Dyne"));
template.update(User.class).matching(where("id").is("Wasp"))
   .apply(update).first()
db.collection.update({
  "_id" : "Wasp"
},
{
  "$set" {
    "firstname" : "Janet",
    "lastname" : "van Dyne",
  }
},
{ ... }
)

未解包对象上的聚合

聚合框架将尝试映射类型化聚合的非包装值。 引用其某个值时,请务必使用包含包装对象的属性路径。 除此之外,无需执行其他特殊操作。spring-doc.cadn.net.cn

未包装对象上的索引

可以像对普通对象一样,将 @Indexed 注解附加到展开类型(unwrapped type)的属性上。 但不能在所属属性上同时使用 @Indexed@Unwrapped 注解。spring-doc.cadn.net.cn

public class User {

	@Id
    private String userId;

    @Unwrapped(onEmpty = USE_NULL)
    UserName name;                    (1)

    // Invalid -> InvalidDataAccessApiUsageException
    @Indexed                          (2)
    @Unwrapped(onEmpty = USE_Empty)
    Address address;
}

public class UserName {

    private String firstname;

    @Indexed
    private String lastname;           (1)
}
1 lastname 集合中为 users 创建了索引。
2 无效的 @Indexed 用法与 @Unwrapped 一起使用