解包类型
解包实体(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;
}
{
"_id" : "1da2ba06-3ba7",
"firstname" : "Emma",
"lastname" : "Frost"
}
当加载
name属性时,如果firstname和lastname都为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;
}
{
"_id" : "a6a805bd-f95f",
"u_firstname" : "Jean", 1
"u_lastname" : "Grey",
"a_firstname" : "Something", 2
"a_lastname" : "Else"
}
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;
}
{
"_id" : "2647f7b9-89da",
"u-first-name" : "Barbara", 2
"u-last-name" : "Gordon"
}
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);
db.collection.find({
"firstname" : "Carol",
"lastname" : "Danvers"
})
也可以直接使用其属性名称访问展开对象的任何字段,如下面的代码片段所示。
示例 5. 对未包装对象的字段进行查询
Query findByUserFirstName = query(where("name.firstname").is("Shuri"));
List<User> users = template.findAll(findByUserFirstName, User.class);
db.collection.find({
"firstname" : "Shuri"
})
按未包装字段排序
未包装对象的字段可以通过其属性路径进行排序,如下例所示。
示例 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 })
虽然可能,但使用未包装的对象本身作为排序标准会包含其所有字段,且顺序不可预测,可能导致排序不准确。
对未包装对象的字段投影
未包装对象的字段可以作为整体进行投影,也可以通过单个字段进行投影,如下面的示例所示。
示例 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
})
对未包装对象进行字段投影时,会包含其所有属性。
示例 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
})
对未包装对象进行字段投影时,将包含其所有属性。
对未包装对象进行按示例查询
在 Example 探针中,未包装的对象可以像其他类型一样使用。请查看按示例查询部分,以了解更多关于此功能的信息。
对未包装对象的存储库查询
Repository 抽象允许在未解包对象的字段以及整个对象上派生查询。
示例 9. 对未包装对象的仓库查询。
interface UserRepository extends CrudRepository<User, String> {
List<User> findByName(UserName username); 1
List<User> findByNameFirstname(String firstname); 2
}
匹配解包对象的所有字段。
匹配
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()
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",
}
},
{ ... }
)
对未包装对象的聚合
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
}
为
users集合中的lastname字段创建了索引。在
@Unwrapped的同时使用了无效的@Indexed。