我正在尝试为嵌套数组token
中apps.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
或为空的情况?
答案 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")
))
);