跳到主要内容

解包类型

DeepSeek V3 中英对照 Unwrapping Types

解包实体(Unwrapped entities)用于在你的 Java 领域模型中设计值对象(value objects),这些值对象的属性会扁平化地存储在其父级的 MongoDB 文档中。

解包类型映射

考虑以下领域模型,其中 User.name 被注解为 @Unwrapped@Unwrapped 注解表示 UserName 的所有属性都应扁平化到拥有 name 属性的 user 文档中。

示例 1. 解包对象的示例代码

class User {

@Id
String userId;

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

class UserName {

String firstname;

String lastname;

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

为了减少冗长的嵌入式类型声明,可以使用 @Unwrapped.Nullable@Unwrapped.Empty 来代替 @Unwrapped(onEmpty = USE_NULL)@Unwrapped(onEmpty = USE_EMPTY)。这两个注解都使用了 JSR-305 的 @javax.annotation.Nonnull 元注解,以帮助进行空值检查。

注意

在未包装的对象中使用复杂类型是可行的。然而,这些复杂类型本身不能是未包装的字段,也不能包含未包装的字段。

解包类型字段名称

通过使用 @Unwrapped 注解的可选 prefix 属性,可以多次解包一个值对象。这样做时,所选的前缀会被添加到解包对象中的每个属性或 @Field("…") 名称之前。需要注意的是,如果多个属性渲染到相同的字段名称,这些值将相互覆盖。

示例 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;
}
java
{
"_id" : "a6a805bd-f95f",
"u_firstname" : "Jean", 1
"u_lastname" : "Grey",
"a_firstname" : "Something", 2
"a_lastname" : "Else"
}
json
  • UserName 的所有属性都以 u_ 为前缀。

  • UserName 的所有属性都以 a_ 为前缀。

在同一个属性上同时使用 @Field@Unwrapped 注解是没有意义的,因此会导致错误。但在解包类型的任何属性上使用 @Field 注解是完全有效的方法。

示例 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;
}
java
{
"_id" : "2647f7b9-89da",
"u-first-name" : "Barbara", 2
"u-last-name" : "Gordon"
}
json
  • UserName 的所有属性都以 u- 为前缀。

  • 最终的字段名称是 @Unwrapped(prefix)@Field(name) 连接的结果。

对未包装对象的查询

在类型级别和字段级别都可以对未包装的属性定义查询,因为提供的 Criteria 会与域类型进行匹配。在渲染实际查询时,前缀和可能的自定义字段名称将被考虑。使用未包装对象的属性名称来匹配所有包含的字段,如下例所示。

示例 4. 对未包装对象进行查询

UserName userName = new UserName("Carol", "Danvers")
Query findByUserName = query(where("name").is(userName));
User user = template.findOne(findByUserName, User.class);
java
db.collection.find({
"firstname" : "Carol",
"lastname" : "Danvers"
})
json

也可以直接使用其属性名称访问展开对象的任何字段,如下面的代码片段所示。

示例 5. 对未包装对象的字段进行查询

Query findByUserFirstName = query(where("name.firstname").is("Shuri"));
List<User> users = template.findAll(findByUserFirstName, User.class);
java
db.collection.find({
"firstname" : "Shuri"
})
json

按未包装字段排序

未包装对象的字段可以通过其属性路径进行排序,如下例所示。

示例 6. 对未包装的字段进行排序

Query findByUserLastName = query(where("name.lastname").is("Romanoff"));
List<User> user = template.findAll(findByUserName.withSort(Sort.by("name.firstname")), User.class);
java
db.collection.find({
"lastname" : "Romanoff"
}).sort({ "firstname" : 1 })
json
备注

虽然可能,但使用未包装的对象本身作为排序标准会包含其所有字段,且顺序不可预测,可能导致排序不准确。

对未包装对象的字段投影

未包装对象的字段可以作为整体进行投影,也可以通过单个字段进行投影,如下面的示例所示。

示例 7. 展开对象的项目。

Query findByUserLastName = query(where("name.firstname").is("Gamora"));
findByUserLastName.fields().include("name"); 1
List<User> user = template.findAll(findByUserName, User.class);
java
db.collection.find({
"lastname" : "Gamora"
},
{
"firstname" : 1,
"lastname" : 1
})
json
  • 对未包装对象进行字段投影时,会包含其所有属性。

示例 8. 在展开对象的字段上进行投影。

Query findByUserLastName = query(where("name.lastname").is("Smoak"));
findByUserLastName.fields().include("name.firstname"); 1
List<User> user = template.findAll(findByUserName, User.class);
java
db.collection.find({
"lastname" : "Smoak"
},
{
"firstname" : 1
})
json
  • 对未包装对象进行字段投影时,将包含其所有属性。

对未包装对象进行按示例查询

Example 探针中,未包装的对象可以像其他类型一样使用。请查看按示例查询部分,以了解更多关于此功能的信息。

对未包装对象的存储库查询

Repository 抽象允许在未解包对象的字段以及整个对象上派生查询。

示例 9. 对未包装对象的仓库查询。

interface UserRepository extends CrudRepository<User, String> {

List<User> findByName(UserName username); 1

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

  • 匹配 firstname 字段。

备注

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

关于未包装对象的更新

解包对象可以像领域模型中的任何其他对象一样进行更新。映射层负责将结构扁平化到其周围的环境中。可以更新解包对象的单个属性,也可以更新整个值,如下面的示例所示。

示例 10. 更新一个未包装对象的单个字段。

Update update = new Update().set("name.firstname", "Janet");
template.update(User.class).matching(where("id").is("Wasp"))
.apply(update).first()
java
db.collection.update({
"_id" : "Wasp"
},
{
"$set" { "firstname" : "Janet" }
},
{ ... }
)
json

示例 11. 更新一个未包装的对象。

Update update = new Update().set("name", new Name("Janet", "van Dyne"));
template.update(User.class).matching(where("id").is("Wasp"))
.apply(update).first()
java
db.collection.update({
"_id" : "Wasp"
},
{
"$set" {
"firstname" : "Janet",
"lastname" : "van Dyne",
}
},
{ ... }
)
json

对未包装对象的聚合

Aggregation Framework 会尝试映射类型化聚合的未包装值。请确保在引用其中一个值时,使用包含包装对象的属性路径。除此之外,不需要其他特殊操作。

未包装对象的索引

可以将 @Indexed 注解附加到解包类型(unwrapped type)的属性上,就像对常规对象所做的那样。但是,不能在拥有属性的同时使用 @Indexed@Unwrapped 注解。

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
}
java
  • users 集合中的 lastname 字段创建了索引。

  • @Unwrapped 的同时使用了无效的 @Indexed