通过传递 ID 列表,基于嵌套的 objectId 检索 Mongo 文档
Retrieving Mongo Documents based on nested objectIds by passing list of ids
我在 MongoDB 中有两个合集,Parent an 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;DR
将内部文档中的id字段注释为@MongoId
。
更长的答案
如本 answer 中所述,您通常不需要 child 文档中的 _id
字段:
The _id field is a required field of the parent document, and is typically not necessary or present in embedded documents. If you require a unique identifier, you can certainly create them, and you may use the _id field to store them if that is convenient for your code or your mental model; more typically, they are named after what they represent (e.g. "username", "otherSystemKey", etc). Neither MongoDB itself, nor any of the drivers will automatically populate an _id field except on the top-level document.
换句话说,与 RDMBS "normalized" 模式不同,此处您不需要 children 文档具有唯一 ID。
您可能仍然希望 children 文档包含某种 "id" 字段,该字段引用另一个集合中的文档(例如 "GoodBoys")。
但实际上,无论出于何种原因,您可能需要在那些内部 children 文档中使用 "unique" 字段。
以下模型将支持提议的结构:
@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 列表检索 parents:
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);
}
添加一些 parent,例如:
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 中的 parent id 是一个实际的 ObjectId,实际上是唯一的。
- Mongo 中的 child id 是一个字符串,我们可以假设 是唯一的。
这种方法在这种情况下效果很好,在这种情况下,我们要么用 new ObjectId().toString()
将 children id 从 Java 插入到 MongoDB,要么以任何其他方式,只要生成的 children id 只是 MongoDB 中的一个字符串。
这意味着 children id 严格来说并不是 MongoDB 中的 ObjectId。
@Id 在Child
如果我们用 @MongoId
注释 children id 字段,结果查询将类似于:
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
如果我们用 @Id
注释 children 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,因此它会尝试 "cast" 我们的字符串到 $oid
.
问题在于,在 MongoDB 中,children 的 _id 属性只是一个字符串,而不是 MongoDB 的 ObjectId。因此,我们的存储库方法将找不到我们的 parent!
所有 ObjectId()
如果我们向 MongoDB 插入一个新文档,其中 child _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
带注释的 child _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
:
注释 child _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
再一次,正如本答案顶部所建议的那样,我不鼓励您在 children 文档中使用 ObjectId。
一些结论
如上所述,我不鼓励任何人在 children 文档中使用 ObjectId。
我们 MongoDB 集合中的一个 'parents' 条目是独一无二的。该文档包含的内容可能代表也可能不代表我们 Java 应用程序中的 Parent
实体。
这是 NoSQL 的支柱之一,与我们将 "tend to" 规范化我们的模式的 RDBMS 相比。
从这个角度来看,不存在“文档中的部分信息是唯一的”这样的事情,嵌套的children是独一无二。
children 最好称为 "children element"(也许有更好的名称)而不是 "chilren documents",因为它们不是实际文档,而是 [=157= 的一部分] 文件(对于那些 parents 恰好在其结构中有一个 "child" 元素)。
如果我们仍然想以某种方式 "link" 嵌套的 children 元素到另一个集合中的 "exclusive"(或唯一)文档,我们确实可以这样做(见下文)。
我们只是
从嵌套元素引用另一个集合中的文档
我认为这是更好的做法。
这个想法是集合中的文档包含我们代表实体所需的一切。
例如,为了表示 parents,除了他们的姓名、年龄和 parents 的任何其他特定信息外,我们可能只想知道他们的名字child(在一个 parent 只有一个 child 的世界里)。
如果我们需要在某个时候访问这些 children 的更多信息,我们可能会有另一个包含 children 详细信息的集合。因此,我们可以 "link" 或 "refer" 从 parents 集合中的嵌套 children 到 children 集合中的文档。
我们的 Java 模型对于 Children:
看起来像这样
@Data
@Builder
@Document(collection = "children")
public class Child {
@Id
private String id;
private String name;
private String key;
private Integer age;
}
一个Parent
实体不会h与 Child
实体有任何 "hard" 关系:
@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;
}
}
请注意,在这种情况下,我更喜欢将嵌套的 children objects 命名为 Parent.ChildData
,因为它只包含我需要的有关 child 的信息代表一个 Parent 实体。
还要注意,我没有用 @Id
注释嵌套的 child id 字段。按照惯例,在这种情况下,MappingMongoConverter
无论如何都会将名为 id
的字段映射到 mongo _id
字段。
鉴于 MongoDB 中嵌套的 child id 和 children id 之间没有关系(正如我们在 RDBMS 中理解的那样),我们甚至可以重命名 ChildData.id
字段到 ChildData.link
。
您可以在 LinkitAir PoC.
中看到这个想法的示例
在那里,Flight
实体模型(存储在 MongoDB 中的 flights
集合中)包含一个嵌套的 AirportData
文档,该文档通过其 code
字段 "refers" 到 Airport
个实体(存储在 airports
集合中的 MongoDB 中)。
我在 MongoDB 中有两个合集,Parent an 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;DR
将内部文档中的id字段注释为@MongoId
。
更长的答案
如本 answer 中所述,您通常不需要 child 文档中的 _id
字段:
The _id field is a required field of the parent document, and is typically not necessary or present in embedded documents. If you require a unique identifier, you can certainly create them, and you may use the _id field to store them if that is convenient for your code or your mental model; more typically, they are named after what they represent (e.g. "username", "otherSystemKey", etc). Neither MongoDB itself, nor any of the drivers will automatically populate an _id field except on the top-level document.
换句话说,与 RDMBS "normalized" 模式不同,此处您不需要 children 文档具有唯一 ID。
您可能仍然希望 children 文档包含某种 "id" 字段,该字段引用另一个集合中的文档(例如 "GoodBoys")。
但实际上,无论出于何种原因,您可能需要在那些内部 children 文档中使用 "unique" 字段。 以下模型将支持提议的结构:
@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 列表检索 parents:
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);
}
添加一些 parent,例如:
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 中的 parent id 是一个实际的 ObjectId,实际上是唯一的。
- Mongo 中的 child id 是一个字符串,我们可以假设 是唯一的。
这种方法在这种情况下效果很好,在这种情况下,我们要么用 new ObjectId().toString()
将 children id 从 Java 插入到 MongoDB,要么以任何其他方式,只要生成的 children id 只是 MongoDB 中的一个字符串。
这意味着 children id 严格来说并不是 MongoDB 中的 ObjectId。
@Id 在Child
如果我们用 @MongoId
注释 children id 字段,结果查询将类似于:
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
如果我们用 @Id
注释 children 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,因此它会尝试 "cast" 我们的字符串到 $oid
.
问题在于,在 MongoDB 中,children 的 _id 属性只是一个字符串,而不是 MongoDB 的 ObjectId。因此,我们的存储库方法将找不到我们的 parent!
所有 ObjectId()
如果我们向 MongoDB 插入一个新文档,其中 child _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
带注释的 child _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
:
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
再一次,正如本答案顶部所建议的那样,我不鼓励您在 children 文档中使用 ObjectId。
一些结论
如上所述,我不鼓励任何人在 children 文档中使用 ObjectId。
我们 MongoDB 集合中的一个 'parents' 条目是独一无二的。该文档包含的内容可能代表也可能不代表我们 Java 应用程序中的 Parent
实体。
这是 NoSQL 的支柱之一,与我们将 "tend to" 规范化我们的模式的 RDBMS 相比。
从这个角度来看,不存在“文档中的部分信息是唯一的”这样的事情,嵌套的children是独一无二。
children 最好称为 "children element"(也许有更好的名称)而不是 "chilren documents",因为它们不是实际文档,而是 [=157= 的一部分] 文件(对于那些 parents 恰好在其结构中有一个 "child" 元素)。
如果我们仍然想以某种方式 "link" 嵌套的 children 元素到另一个集合中的 "exclusive"(或唯一)文档,我们确实可以这样做(见下文)。 我们只是
从嵌套元素引用另一个集合中的文档
我认为这是更好的做法。
这个想法是集合中的文档包含我们代表实体所需的一切。
例如,为了表示 parents,除了他们的姓名、年龄和 parents 的任何其他特定信息外,我们可能只想知道他们的名字child(在一个 parent 只有一个 child 的世界里)。
如果我们需要在某个时候访问这些 children 的更多信息,我们可能会有另一个包含 children 详细信息的集合。因此,我们可以 "link" 或 "refer" 从 parents 集合中的嵌套 children 到 children 集合中的文档。
我们的 Java 模型对于 Children:
看起来像这样@Data
@Builder
@Document(collection = "children")
public class Child {
@Id
private String id;
private String name;
private String key;
private Integer age;
}
一个Parent
实体不会h与 Child
实体有任何 "hard" 关系:
@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;
}
}
请注意,在这种情况下,我更喜欢将嵌套的 children objects 命名为 Parent.ChildData
,因为它只包含我需要的有关 child 的信息代表一个 Parent 实体。
还要注意,我没有用 @Id
注释嵌套的 child id 字段。按照惯例,在这种情况下,MappingMongoConverter
无论如何都会将名为 id
的字段映射到 mongo _id
字段。
鉴于 MongoDB 中嵌套的 child id 和 children id 之间没有关系(正如我们在 RDBMS 中理解的那样),我们甚至可以重命名 ChildData.id
字段到 ChildData.link
。
您可以在 LinkitAir PoC.
中看到这个想法的示例
在那里,Flight
实体模型(存储在 MongoDB 中的 flights
集合中)包含一个嵌套的 AirportData
文档,该文档通过其 code
字段 "refers" 到 Airport
个实体(存储在 airports
集合中的 MongoDB 中)。