嵌套数组中的MongoDB可选部分唯一索引

时间:2020-09-10 22:11:32

标签: mongodb mongodb-java

我正在尝试为嵌套数组tokenapps.tokens字段上的唯一局部索引创建解决方案,以使嵌套数组tokens是可选的或可以为空。 / p>

我将索引创建为:

collection.createIndex(
        Indexes.ascending("apps.tokens.token"),
        new IndexOptions()
                .unique(true)
                .partialFilterExpression(
                        Filters.type("apps.tokens.token", BsonType.STRING)
                )
);

字段apps.tokens.token的值永远不会显式null,并且始终是一些唯一的字符串。我目前不担心同一文档中的重复项。

但是,我无法使部分索引按我期望的方式运行。除了在apps数组中有一个项目为空或缺少tokens数组的情况下,它基本上可以按预期工作。

创建以下结构失败,错误为E11000 duplicate key error collection: db1.testCollection index: apps.tokens.token_1 dup key: { apps.tokens.token: null }

[
    {
        "apps": [
            {
                "client_id": "capp1",
                "tokens": [
                    {
                        "token": "t1",
                        "expiration": "2020-09-10T23:31:17.119+01:00"
                    }
                ]
            },
            {
                "client_id": "capp2"
            }
        ],
        "uuid": "89337f58-a491-4e17-b8dd-726c9319dcaa"
    },
    {
        "apps": [
            {
                "client_id": "capp3",
                "tokens": [
                    {
                        "token": "t2",
                        "expiration": "2020-09-10T23:31:17.119+01:00"
                    }
                ]
            },
            {
                "client_id": "capp4"
            }
        ],
        "uuid": "4ccc4d81-990f-4650-b26e-1d26fd22d91a"
    }
]

但是,根据相同的索引,此结构完全有效:

[
    {
        "apps": [
            {
                "client_id": "capp1"
            },
            {
                "client_id": "capp2"
            }
        ],
        "uuid": "89337f58-a491-4e17-b8dd-726c9319dcaa"
    },
    {
        "apps": [
            {
                "client_id": "capp3"
            },
            {
                "client_id": "capp4"
            }
        ],
        "uuid": "4ccc4d81-990f-4650-b26e-1d26fd22d91a"
    }
]

我的猜测是第一个测试用例失败,因为在插入第一个项目后,索引会检查它是否具有一个apps.token.token字段(它是一个字符串),并将整个文档添加到插入/更新比较中。

另一方面,第二个测试用例也不会失败,因为没有一个文档符合apps.tokens.token是字符串的条件。

在查看要插入的第二个项目时,它以某种方式推断出它具有一个apps.token.token隐式的null字段(因为其中一个字段中没有tokens数组apps个项目),然后检查现有项目是否与{"apps.tokens.token": null}相匹配并且确实匹配,并在失败时结束操作。

我在做什么错了?

我也尝试使用exists过滤器创建部分索引,但这无济于事。

Filters.and(
        Filters.type("apps.tokens.token", BsonType.STRING),
        Filters.exists("apps.tokens.token"),
        Filters.exists("apps.tokens")
)

是否可以为过滤器添加某种功能,以处理文档中每个tokens项目不存在apps或为空的情况?

2 个答案:

答案 0 :(得分:1)

MongoDB中索引的目的是将特定值映射到文档。

对于数组上的索引(多键索引),单个文档的索引中将有多个值。

一个例子:

文档

#1 { apps: [
         { tokens: [
                  {token: "T1"},
                  {token: "T2"}
         ]},
         { tokens: [] }
    ]},
#2 { apps: [
         { tokens: [
                  {token: "T3"},
                  {token: "T4"}
         ]},
         { notokens: true }
    ])
#3 { apps: [
         { notokens: true }
         { notokens: true }
   ]}
#4 { apps: [
         { tokens: [
                  { token: "T5" },
                  { token: "T5" }
          ]}
   ]}

索引

如果我们在{"apps.tokens.token": 1}上创建索引,则该索引将具有以下内容:

NULL -> #1
NULL -> #2
NULL -> #3
"T1" -> #1
"T2" -> #1
"T3" -> #2
"T4" -> #2
"T5" -> #4

唯一

如果我们改为使用唯一约束创建该索引,则文档#2和#3都将被拒绝,因为它们会导致NULL值在索引中重复。

请注意,文件#4将被接受。由于输入索引的值必须唯一,并且给定文档的索引值仅索引一次,因此"T5"在索引中不会重复,即使它在文档中出现两次也是如此。不违反唯一约束。

部分

部分索引过滤器与整个文档匹配。如果过滤器匹配,则索引中包含该文档。

如果我们使用部分过滤器{"apps.tokens.token":{$type:"string"}}创建索引,则其匹配的方式与我们将其传递给find的方式相同,即,如果数组中的任何元素匹配,则文档为匹配。

这意味着索引中将包含文档#1,#2和#4,而将#3排除在外。

如果我们使索引同时具有部分索引和唯一索引,则文件#1,#3和#4将被接受,而文件#2将由于复制NULL值而被拒绝。

答案 1 :(得分:0)

尽管官方文档指出,看来解决方案可能是使用sparse索引。

部分索引提供了稀疏功能的超集 索引。如果您使用的是MongoDB 3.2或更高版本,则应使用部分索引 优于稀疏索引。

我的测试通过:

collection.createIndex(
        Indexes.ascending("apps.tokens.token"),
        new IndexOptions()
                .unique(true)
                .sparse(true)
);

我想知道这是否还有其他目前尚不明显的含义。

为解决方案的完整,请注意,索引处理的是文档之间的唯一性。但是,它不会检查同一文档中的唯一性,因此可以添加在同一文档中的一个应用程序中已经存在的令牌。要变通解决此问题,我向更新查询中添加了一个筛选器,以使已经具有要添加的令牌的文档不包含在要更新的文档中:

Document doc = Document.parse("{\"token\":\"t1\"}");
collection.updateOne(
        Filters.and(
                Filters.eq("uuid", "89337f58-a491-4e17-b8dd-726c9319dcaa"),
                Filters.not(Filters.eq("apps.tokens.token", "t1"))
        ),

        Updates.push("apps.$[app].tokens", doc),
        new UpdateOptions().arrayFilters(Arrays.asList(
                Filters.eq("app.client_id", "capp1")
        ))
);