【Spring连载】使用Spring Data访问 MongoDB----对象映射之对象引用
一、使用DBRefs
映射框架不必存储嵌入document中的子对象。你也可以将它们单独存储,并使用DBRef引用该document。当从MongoDB加载对象时,这些引用会被急切地解析,这样你就可以返回一个映射的对象,看起来就像它被嵌入到顶级document中一样。
以下示例使用DBRef来引用独立于引用它的对象而存在的特定document(为了简洁起见,这两个类都以内联方式显示):
@Document
public class Account {
@Id
private ObjectId id;
private Float total;
}
@Document
public class Person {
@Id
private ObjectId id;
@Indexed
private Integer ssn;
@DBRef
private List<Account> accounts;
}
你不需要使用@OneToMany或类似的机制,因为对象列表告诉映射框架你想要一对多关系。当对象存储在MongoDB中时,会有一个DBRefs列表,而不是Account对象本身。在加载DBRef集合时,建议将集合类型中的引用限制为特定的MongoDB集合。这允许批量加载所有引用,而指向不同MongoDB集合的引用需要逐个解析。
映射框架不处理级联保存。如果更改Person对象引用的Account对象,则必须单独保存Account对象。对Person对象调用save不会自动将Account对象保存在accounts属性中。
DBRefs也可以延迟解析。在这种情况下,引用的实际对象或集合是在首次访问属性时解析的。使用@DBRef的lazy属性来指定这一点。被定义为延迟加载DBRef并用作构造函数参数的必需属性也使用延迟加载代理进行装饰,以确保尽可能少地给数据库和网络带来压力。
延迟加载的DBRefs可能很难调试。确保工具不会意外触发代理解析,例如调用toString()或一些内联调试渲染调用属性getters。请考虑为org.springframework.data.mongodb.core.convert.DefaultDbRefResolver启用trace级别的日志记录,以深入了解DBRef解析。
延迟加载可能需要类代理,而类代理反过来可能需要访问jdk内部(internals),这些内部不是打开的,从Java 16+开始,因为JEP 396:默认情况下强封装jdk内部。对于这些情况,请考虑回退到接口类型(例如,从ArrayList切换到List)或提供所需的–add-opens参数。
二、使用Document引用
使用@DocumentReference提供了一种灵活的方式来引用MongoDB中的实体。虽然目标与使用DBRefs时相同,但存储表示方式不同。DBRef解析为一个具有固定结构的文档,如MongoDB参考文档中所述。
Document引用,不遵循特定格式。它们可以是任何东西,一个值,一个完整的document,基本上是可以存储在MongoDB中的所有东西。默认情况下,映射层将使用引用的实体id值进行存储和检索,如下面的示例所示。
@Document
class Account {
@Id
String id;
Float total;
}
@Document
class Person {
@Id
String id;
@DocumentReference --------1
List<Account> accounts;
}
Account account = …
template.insert(account); --------2
template.update(Person.class)
.matching(where("id").is(…))
.apply(new Update().push("accounts").value(account)) --------3
.first();
{
"_id" : …,
"accounts" : [ "6509b9e" … ] --------4
}
1. 标记要引用的Account值的集合。
2. 映射框架不处理级联保存,因此请确保单独保存引用的实体。
3. 添加对现有实体的引用。
4. 引用的Account实体表示为其_id值的数组。
上面的示例使用基于_id的提取查询({ ‘_id’ : ?#{#target} })进行数据检索,并及时地解析链接的实体。可以使用@DocumentReference的属性更改解析默认值(如下所列)
表1:@DocumentReference默认值
Attribute | Description | Default |
---|---|---|
db | 用于集合查找的目标数据库名称. | MongoDatabaseFactory.getMongoDatabase() |
collection | 目标集合名称。 | 带注解的属性的域类型,分别是类似集合或Map属性的值类型,集合名称。 |
lookup | 使用#target作为给定源值的标记,通过SpEL表达式计算占位符的单个document查找查询。类似集合或Map的属性通过$or运算符组合各个查找。 | 使用加载的源值的基于_id字段的查询({ ‘_id’ : ?#{#target} })。 |
sort | 用于在服务器端对结果documents进行排序。 | 默认情况下无。类似集合的属性的结果顺序将基于所使用的查找查询以尽最大努力的方式恢复。 |
lazy | 如果设置为true,则在首次访问属性时会延迟解析。 | 默认情况下立即解析属性。 |
@DocumentReference(lookup)允许定义与_id字段不同的过滤器查询,因此提供了一种灵活的方式来定义实体之间的引用,如下面的示例所示,其中书籍的Publisher通过其首字母缩略词而不是内部id来引用。
@Document
class Book {
@Id
ObjectId id;
String title;
List<String> author;
@Field("publisher_ac")
@DocumentReference(lookup = "{ 'acronym' : ?#{#target} }") --------1
Publisher publisher;
}
@Document
class Publisher {
@Id
ObjectId id;
String acronym; --------1
String name;
@DocumentReference(lazy = true) --------2
List<Book> books;
}
1. 使用acronym字段查询Publisher集合中的实体。
2. 延迟加载指向Book集合的引用。
Book document
{
"_id" : 9a48e32,
"title" : "The Warded Man",
"author" : ["Peter V. Brett"],
"publisher_ac" : "DR"
}
Publisher document
{
"_id" : 1a23e45,
"acronym" : "DR",
"name" : "Del Rey",
…
}
上面的片段显示了使用自定义引用对象时的读取部分。写入需要一些额外的设置,因为映射信息无法表达#target的来源。映射层需要在目标document和DocumentPointer之间注册转换器,如下所示:
@WritingConverter
class PublisherReferenceConverter implements Converter<Publisher, DocumentPointer<String>> {
@Override
public DocumentPointer<String> convert(Publisher source) {
return () -> source.getAcronym();
}
}
如果没有提供DocumentPointer转换器,则可以根据给定的查找查询计算目标引用document。在这种情况下,关联目标属性的评估如下面的示例所示。
@Document
class Book {
@Id
ObjectId id;
String title;
List<String> author;
@DocumentReference(lookup = "{ 'acronym' : ?#{acc} }") --------1,2
Publisher publisher;
}
@Document
class Publisher {
@Id
ObjectId id;
String acronym; --------1
String name;
// ...
}
1. 使用acronym字段查询Publisher集合中的实体。
2. 查找查询的字段值占位符(如acc)用于形成引用document。
{
"_id" : 9a48e32,
"title" : "The Warded Man",
"author" : ["Peter V. Brett"],
"publisher" : {
"acc" : "DOC"
}
}
也可以使用@ReadonlyProperty和@DocumentReference的组合来建模关系样式的一对多引用。这种方法允许链接类型不需要将链接值存储在拥有的(owning )document中,而是存储在引用的文档中,如下面的示例所示。
@Document
class Book {
@Id
ObjectId id;
String title;
List<String> author;
ObjectId publisherId; --------1
}
@Document
class Publisher {
@Id
ObjectId id;
String acronym;
String name;
@ReadOnlyProperty --------2
@DocumentReference(lookup="{'publisherId':?#{#self._id} }") --------3
List<Book> books;
}
1. 通过将Publisher.id存储在Book document中,设置从Book(引用)到Publisher(所有者)的链接。
2. 将包含引用的属性标记为只读。这将阻止在Publisher文档中存储对个别书籍的引用。
3. 使用#self变量访问Publisher文档中的值,并在此检索具有匹配publisherId的图书。
Book document
{
"_id" : 9a48e32,
"title" : "The Warded Man",
"author" : ["Peter V. Brett"],
"publisherId" : 8cfb002
}
Publisher document
{
"_id" : 8cfb002,
"acronym" : "DR",
"name" : "Del Rey"
}
有了以上所有这些,就可以对实体之间的所有类型的关联进行建模。看看下面的非详尽的示例列表,感受一下什么是可能的。
例1:使用id字段的简单文档引用
class Entity {
@DocumentReference
ReferencedObject ref;
}
// entity
{
"_id" : "8cfb002",
"ref" : "9a48e32" --------1
}
// referenced object
{
"_id" : "9a48e32" --------1
}
1. MongoDB简单类型可以直接使用,无需进一步配置。
例2:简单Document引用使用id字段与显式查找查询
class Entity {
@DocumentReference(lookup = "{ '_id' : '?#{#target}' }") --------1
ReferencedObject ref;
}
// entity
{
"_id" : "8cfb002",
"ref" : "9a48e32" --------1
}
// referenced object
{
"_id" : "9a48e32"
}
1. target定义了引用值本身。
例3:Document引用为查找查询提取refKey字段
class Entity {
@DocumentReference(lookup = "{ '_id' : '?#{refKey}' }") --------1,2
private ReferencedObject ref;
}
@WritingConverter
class ToDocumentPointerConverter implements Converter<ReferencedObject, DocumentPointer<Document>> {
public DocumentPointer<Document> convert(ReferencedObject source) {
return () -> new Document("refKey", source.id); --------1
}
}
// entity
{
"_id" : "8cfb002",
"ref" : {
"refKey" : "9a48e32" --------1
}
}
// referenced object
{
"_id" : "9a48e32"
}
1. 用于获取引用值的键必须是写入时使用的键。
2. refKey是target.refKey的缩写。
例4:包含多个值的Document引用,形成查找查询
class Entity {
@DocumentReference(lookup = "{ 'firstname' : '?#{fn}', 'lastname' : '?#{ln}' }") --------1,2
ReferencedObject ref;
}
// entity
{
"_id" : "8cfb002",
"ref" : {
"fn" : "Josh", --------1
"ln" : "Long" --------1
}
}
// referenced object
{
"_id" : "9a48e32",
"firstname" : "Josh", --------2
"lastname" : "Long", --------2
}
1. 根据查找查询从链接document中读写键fn和ln。
2. 使用非id字段查找目标documents。
例5:从目标集合读取Document引用
class Entity {
@DocumentReference(lookup = "{ '_id' : '?#{id}' }", collection = "?#{collection}") --------2
private ReferencedObject ref;
}
@WritingConverter
class ToDocumentPointerConverter implements Converter<ReferencedObject, DocumentPointer<Document>> {
public DocumentPointer<Document> convert(ReferencedObject source) {
return () -> new Document("id", source.id) --------1
.append("collection", … ); --------2
}
}
// entity
{
"_id" : "8cfb002",
"ref" : {
"id" : "9a48e32", --------1
"collection" : "…" --------2
}
}
1. 从引用文档中读取/写入键_id,以便在查找查询中使用它们。
2. 集合名称可以使用其键从引用document中读取。
我们知道在查找查询中使用各种MongoDB查询运算符是很好的。但有几个方面需要考虑:
- 确保有适当的索引来支持你的查找。
- 请注意,解析需要服务器交互,这会导致延迟,请考虑延迟策略。
- document引用的集合是使用$or运算符批量加载的。
在尽最大努力的基础上,在内存中恢复原始元素顺序。只有在使用相等表达式时才能恢复顺序,而在使用MongoDB查询运算符时无法恢复顺序。在这种情况下,结果将在从存储收到时进行排序,或通过提供的@DocumentReference(sort)属性进行排序。
还有几个注意点:
- 你使用循环引用吗?问问自己是否需要它们。
- 延迟的document引用很难调试。确保工具不会通过调用toString()意外触发代理解析。
- 不支持使用反应式(reactive)基础结构读取document引用。