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

MongoDB Optional Partial Unique Index in Nested Arrays

我正在尝试为嵌套数组 apps.tokenstoken 字段的唯一部分索引创建一个解决方案,这样嵌套数组 tokens 是可选的或可以为空.

我将索引创建为:

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 字段,该字段是一个字符串,并将整个文档添加到 insert/update比较。

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

当它查看要插入的第二个项目时,它以某种方式推断出它有一个隐式 nullapps.token.token 字段(因为其中没有 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")
)

是否可以用某种函数来补充过滤器,以处理文档中每个 appstokens 不存在或为空的情况?

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 值而被拒绝。

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

Partial indexes offer a superset of the functionality of sparse indexes. If you are using MongoDB 3.2 or later, partial indexes should be preferred over sparse indexes.

我的测试通过了:

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