mongodb - 通过传递 id 列表基于嵌套 objectIds 检索 Mongo 文档
问题描述
我在 MongoDB 中有两个集合,Parent 和 Child。下面是文档的结构。
Parent: {
'_id': 'some_value',
'name': 'some_value',
'child': {
'_id': 'some_value',
'name': 'some_value',
'key':'value'
}
}
我正在尝试在 MongoRepository 方法中传递子 ID 列表,以便检索父对象但获取空值。下面是我的代码。
import org.bson.types.ObjectId;
class MyRepository extends CrudRepository<Parent,Long> {
@Query("{'child._id': {$in : ?0 }}")
List<Parent> findByChild_IdIn(List<ObjectId> childIds);
}
我正在调用我的方法,如下所示。
import org.bson.types.ObjectId;
List<String> childrenIds = getChildrenIdList();
List<ObjectId> childIds = childrenIds.stream().map(childrenId -> new ObjectId(childrenId)).collect(Collectors.toList());
List<Parent> parentsList = myRepository.findByChild_IdIn(childIds);
我在这里做错了什么?为什么它给出空值。
解决方案
TL;博士
用 注释内部文档中的id@MongoId
字段。
更长的答案
如this answer中所述,您通常不需要_id
子文档中的字段:
_id 字段是父文档的必填字段,通常不需要或不存在于嵌入文档中。如果你需要一个唯一的标识符,你当然可以创建它们,如果你的代码或你的心智模型方便,你可以使用 _id 字段来存储它们;更典型的是,它们以它们所代表的内容命名(例如“用户名”、“其他系统密钥”等)。MongoDB 本身和任何驱动程序都不会自动填充 _id 字段,除了顶级文档。
换句话说,与 RDMBS“规范化”模式不同,这里您不需要子文档具有唯一 ID。
您可能仍然希望您的子文档包含某种“id”字段,该字段引用另一个集合中的文档(例如“GoodBoys”)。
但实际上,无论出于何种原因,您可能需要在这些内部子文档中使用“唯一”字段。以下模型将支持建议的结构:
@Data
@Builder
@Document(collection = "parents")
public class Parent {
@Id
private String id;
private String name;
private Child child;
@Data
@Builder
public static class Child {
@MongoId
private String id;
private String name;
private String key;
}
}
您可以使用以下任一方式通过 id 列表检索父母:
public interface ParentsRepository extends MongoRepository<Parent, String> {
// QueryDSL
List<Parent> findByChildIdIn(List<String> ids);
// Native Query
@Query("{'child._id': {$in: ?0}}")
List<Parent> findByChildrenIds(List<String> ids);
}
添加一些父母,例如:
parentsRepository.saveAll(Arrays.asList(
Parent.builder()
.name("parent1")
.child(Parent.Child.builder().id(new ObjectId().toString()).name("child1").key("value1").build())
.build(),
Parent.builder()
.name("parent2")
.child(Parent.Child.builder().id(new ObjectId().toString()).name("child2").key("value2").build())
.build()
));
MongoDB 中的结果条目将如下所示:
{
"_id" : ObjectId("5e07384596d9077ccae89a8c"),
"name" : "parent1",
"child" : {
"_id" : "5e07384596d9077ccae89a8a",
"name" : "child1",
"key" : "value1"
},
"_class" : "com.lubumbax.mongoids.model.Parent"
}
这里注意两个重要的事情:
- Mongo 中的父 id 是实际唯一的实际 ObjectId。
- Mongo 中的子 ID 是一个字符串,我们可以假设它是唯一的。
这种方法适用于这种情况,在这种情况下,我们要么使用new ObjectId().toString()
或以任何其他方式将子 id 从 Java 插入到 MongoDB,只要生成的子 id 只是 MongoDB 中的字符串。
这意味着子 ID 在 MongoDB 中并未严格表示为 ObjectId。
孩子中的@Id
如果我们用 注释 children id 字段@MongoId
,结果查询将类似于:
StringBasedMongoQuery: Created query Document{{child._id=Document{{$in=[5e0740e41095314a3401e49c, 5e0740e41095314a3401e49d]}}}} for Document{{}} fields.
MongoTemplate : find using query: { "child._id" : { "$in" : ["5e0740e41095314a3401e49c", "5e0740e41095314a3401e49d"]}} fields: Document{{}} for class: com.lubumbax.mongoids.model.Parent in collection: parents
相反,如果我们用 注释 children id 字段@Id
,则结果查询将是:
StringBasedMongoQuery: Created query Document{{child._id=Document{{$in=[5e0740e41095314a3401e49c, 5e0740e41095314a3401e49d]}}}} for Document{{}} fields.
MongoTemplate : find using query: { "child._id" : { "$in" : [{ "$oid" : "5e0740e41095314a3401e49c"}, { "$oid" : "5e0740e41095314a3401e49d"}]}} fields: Document{{}} for class: com.lubumbax.mongoids.model.Parent in collection: parents
注意$oid
那里。MongoDB Java 驱动程序期望 MongoDB 中的 id 属性是一个实际的 ObjectId,因此它尝试将我们的字符串“强制转换”为$oid
.
问题在于,在 MongoDB 中,子项的 _id 属性只是一个字符串,而不是 MongoDB 的 ObjectId。因此,我们的存储库方法将找不到我们的父级!
所有 ObjectId()
如果我们向 MongoDB 插入一个新文档,其中子 _id 是实际的 ObjectId 而不是字符串,会发生什么?:
> db.parents.insert({
name: "parent3",
child: {
_id: ObjectId(),
name: "child3",
key: "value3"
}
});
结果条目是:
> db.parents.find({});
{
"_id" : ObjectId("5e074233669d34403ed6bcd2"),
"name" : "parent3",
"child" : {
"_id" : ObjectId("5e074233669d34403ed6bcd1"),
"name" : "child3",
"key" : "value3"
}
}
如果我们现在尝试使用带@MongoId
注释的子 _id 字段找到这个,我们将找不到它!
结果查询将是:
StringBasedMongoQuery: Created query Document{{child._id=Document{{$in=[5e074233669d34403ed6bcd1]}}}} for Document{{}} fields.
MongoTemplate : find using query: { "child._id" : { "$in" : ["5e074233669d34403ed6bcd1"]}} fields: Document{{}} for class: com.lubumbax.mongoids.model.Parent in collection: parents
为什么?因为现在 MongoDB 中的 _id 属性是一个实际的 ObjectId,我们试图将它作为纯字符串查询。我们也许可以通过使用 SpEL 调整查询来解决这个问题,但恕我直言,我们正在输入“Land of Pain”。
但是,正如我们所预料的那样,如果我们用以下方式注释子 _id 字段,则可以找到该文档@Id
:
StringBasedMongoQuery: Created query Document{{child._id=Document{{$in=[5e074233669d34403ed6bcd1]}}}} for Document{{}} fields.
MongoTemplate : find using query: { "child._id" : { "$in" : [{ "$oid" : "5e074233669d34403ed6bcd1"}]}} fields: Document{{}} for class: com.lubumbax.mongoids.model.Parent in collection: parents
再一次,正如此答案顶部所建议的那样,我不鼓励您在子文档中使用 ObjectId。
一些结论
如上所述,我不鼓励任何人在子文档中使用 ObjectId。
我们的 MongoDB 集合中的“父母”条目是独一无二的。该文档包含的内容可能代表或可能不代表Parent
我们 Java 应用程序中的实体。
这是 NoSQL 的支柱之一,与 RDBMS 相比,我们会“倾向于”规范化我们的模式。
从这个角度来看,不存在“文档中的部分信息是唯一的”这样的事情,嵌套的孩子是唯一的。
孩子最好被称为“孩子元素”(也许有一个更好的名字)而不是“孩子文档”,因为它们不是实际的文档,而是“父母”文档的一部分(对于那些碰巧的父母在其结构中有一个“子”元素)。
如果我们仍然想以某种方式将嵌套的子元素“链接”到另一个集合中的“排他”(或唯一)文档,我们确实可以这样做(见下文)。我们刚刚
从嵌套元素引用另一个集合中的文档
在我看来,这是一个更好的做法。
这个想法是集合中的文档包含我们表示实体所需的所有内容。
例如,为了代表父母,除了他们的姓名、年龄和父母的任何其他特定信息之外,我们可能只想知道他们孩子的名字(在父母只有一个孩子的世界中)。
如果我们需要在某个时候访问这些孩子的更多信息,我们可能会有另一个包含孩子详细信息的集合。因此,我们可以从 parent 集合中的嵌套子项“链接”或“引用”到 children 集合中的文档。
对于 Children,我们的 Java 模型看起来像这样:
@Data
@Builder
@Document(collection = "children")
public class Child {
@Id
private String id;
private String name;
private String key;
private Integer age;
}
Parent
实体不会与实体有任何“硬”关系Child
:
@Data
@Builder
@Document(collection = "parents")
public class Parent {
@Id
private String id;
private String name;
private ChildData child;
@Data
@Builder
public static class ChildData {
private String id;
private String name;
}
}
请注意,在这种情况下,我更喜欢将嵌套的子对象命名为Parent.ChildData
,因为它只包含我需要的关于子对象的信息,以表示父实体。
另请注意,我没有用@Id
. 按照惯例,MappingMongoConverter
在这种情况下,无论如何都会将调用的字段映射id
到 mongo_id
字段。
鉴于在 MongoDB 中(正如我们在 RDBMS 中理解的那样)嵌套的子 id 和子 id 之间没有关系,我们甚至可以将该ChildData.id
字段重命名为ChildData.link
.
你可以在LinkitAir PoC中看到这个想法的一个例子。
在那里,Flight
实体模型(存储在flights
集合中的 MongoDB 中)包含一个嵌套AirportData
文档,该文档通过其code
字段“引用”Airport
实体(存储在airports
集合中的 MongoDB 中)。
推荐阅读
- mysql - 如何用 HQL 替换 JForm 中的 MySQL 插入参数化查询?
- hadoop - 如何在蜂巢中实现百分位数
- scala - 从地图生成邻接矩阵
- vue.js - 使用搜索功能设置 v-autocomplete
- excel - 使用 Excel 正确计算(和求和)分子量
- mysql - SQL查询中Regex的使用
- spring-boot - ConfigMap 不适用于 application.properties 但适用于 application.yml
- amazon-web-services - 没有 S3 存储桶的 AWS Cloudfront CORS 标头
- selenium-webdriver - Selenium NetworkConditions 和 ApplicationCache 异常
- c# - 在多个线程中工作的队列中的内存问题