通过传递ID列表基于嵌套的objectId检索Mongo Documents

时间:2019-12-18 08:35:42

标签: mongodb spring-data spring-data-mongodb

我在MongoDB中有两个集合,父子。下面是文档的结构。

Parent: {
'_id': 'some_value',
'name': 'some_value',

 'child': {

    '_id': 'some_value',
    'name': 'some_value',
    'key':'value'
    }

}

我正在尝试在MongoRepository方法中传递子ID的列表,以便检索Parent对象,但获取空值。下面是我的代码。

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);

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

1 个答案:

答案 0 :(得分:0)

TL; DR

@MongoId注释内部文档中的 id 字段。

更长的答案

如本answer中所述,您通常不需要在子文档中使用_id字段:

  

_id字段是父文档的必填字段,通常不是必需的,也不存在于嵌入式文档中。如果需要唯一标识符,则可以创建它们,并且如果方便使用代码或思维模型,则可以使用_id字段存储它们。更常见的是,它们以其表示的名称命名(例如“用户名”,“ otherSystemKey”等)。除了顶层文档外,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()或其他方式将Java的子代ID插入到MongoDB中,只要得到的子代ID只是MongoDB中的字符串即可。

这意味着在MongoDB中未严格将子级id表示为ObjectId。

@Id在孩子中

如果我们用@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,因此它将尝试将我们的字符串“投射”到$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调整查询来解决此问题,但是恕我直言,我们正在输入“痛苦之地”。

但是,正如我们期望的那样,如果我们使用@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集合中的“父母”条目是唯一的。该文档包含的内容可能代表我们的Java应用程序中的Parent实体。
与RDBMS相比,这是NoSQL的支柱之一。在RDBMS中,我们倾向于“规范化”我们的模式。

从这个角度来看,没有像“ 文档中的信息的一部分是唯一的”这样的东西,嵌套子元素是唯一的。
最好将孩子称为“孩子元素”(也许对此有一个更好的称呼),而不是“孩子的文件”,因为它们不是实际的文件,而是“父母”文件的一部分(对于那些碰巧遇到的父母)在其结构中包含一个“子”元素)。

如果我们仍然想以某种方式将嵌套的子元素“链接”到另一个集合中的“排他”(或唯一)文档,我们确实可以这样做(见下文)。 我们只是

从嵌套元素中引用另一个集合中的文档

我认为这是更好的做法。
这个想法是集合中的文档包含了我们代表实体所需要的全部内容。

例如,为了代表父母,除了他们的姓名,年龄和其他有关父母的其他信息外,我们可能只想知道他们孩子的名字(在父母只有一个孩子的世界中) 。
如果需要在某个时候访问这些孩子的更多信息,我们可能会提供另一个包含孩子详细信息的集合。因此,我们可以从父集合中的嵌套子对象“链接”或“引用”到子集合中的文档。

对于孩子们,我们的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注释嵌套的子ID字段。按照惯例,在这种情况下,MappingMongoConverter始终会将名为id的字段映射到mongo _id字段。
鉴于在MongoDB中(如我们在RDBMS中所理解的),在嵌套的子ID和子ID之间没有关系,我们甚至可以将ChildData.id字段重命名为ChildData.link

您可以在LinkitAir PoC中看到此想法的示例。
那里,Flight实体模型(存储在MongoDB中的flights集合中)包含一个嵌套的AirportData文档,该文档通过其code字段“引用” Airport实体(存储在MongoDB中的airports集合中)。