解包类型

解包实体用于在您的 Java 域模型中设计值对象,其属性被扁平化到父 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"
}
1 当加载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;
}
{
  "_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注解结合使用没有意义,因此会导致错误。但在解包类型的任何属性上使用@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"
}
1 UserName的所有属性都以u-为前缀。
2 最终的字段名称是将@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
})
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 探测器中使用,就像任何其他类型一样。请查看 示例查询 部分,以了解有关此功能的更多信息。

对未包装对象进行存储库查询。

Repository 抽象允许对未包装对象的字段以及整个对象进行查询。

示例 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,也暂停对未包装对象的索引创建。

更新未包装对象

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

示例 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",
  }
},
{ ... }
)

对未包装对象进行聚合

聚合框架将尝试映射类型化聚合的解包值。请确保在引用其值之一时使用包含包装对象的属性路径。除此之外,无需执行任何特殊操作。

解包对象的索引

可以将@Indexed注解附加到解包类型的属性,就像对普通对象一样。不能将@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)
}
1 users集合中的lastname创建索引。
2 @Indexed@Unwrapped一起使用时的无效用法