首页 > 解决方案 > 通过传递 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);

我在这里做错了什么?为什么它给出空值。

标签: mongodbspring-dataspring-data-mongodb

解决方案


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 中)。


推荐阅读