解包类型
解包实体(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
。