我是一个老SQL,现在正在使用现有的Mongo数据库。
与其使用嵌入文档的数组,关键信息是数组元素中的字段之一,每个嵌入文档都是它自己的字段,键作为字段名。
假设一个名为的集合bank_branches
。而不是这样:
{
branch : "St. Louis",
branch_employees : [
{
name : "Mary",
id : "M12345"
hire_date : Date("2010-05-20")
},
{
name : "John",
id : "J29876",
hire_date : Date("2015-03-23")
}
]
},
{
branch : "Jefferson City",
branch_employees : [
{
name : "Lisa",
id : "L87653"
hire_date : Date("2016-01-07")
}
]
}
…我们有这样的文件:
{
branch : "St. Louis",
branch_employees : {
M12345 : {
name : "Mary",
hire_date : Date("2010-05-20")
},
J29876 : {
name : "John",
hire_date : Date("2015-03-23")
}
}
{
branch : "Jefferson City",
branch_employees : {
L87653 : {
name : "Lisa",
hire_date : Date("2016-01-07")
}
}
}
(这是一个发明的结构来显示问题。)
在MongoDB聚合管道或其他方式中,是否有任何方法可以执行以下任一操作?
>
查询员工嵌入文档的组件字段,这样我就可以,例如,获取2016年入职员工的所有分支机构,而无需提前知道所有嵌入文档的所有字段名?
松开这些对象,这样我就可以拥有一个分支机构员工文档的数组,同样不需要提前知道所有嵌入文档的所有字段名?(MongoDB$unstyle
管线作业只适用于数组。)
我怀疑第一个可能可以用$where
和javascript
和/或自定义javascript存储函数来解决。(我以前从未使用过存储函数。)但我怀疑第二个只能通过编程来解决。
我可以通过编写Python和迭代来满足我的用例。但是我宁愿编写查询来查找记录,也不愿以编程方式过滤它们。(唯一保证bug的代码是你不必编写的代码。)
有人有建议吗?非常感谢,事先。
我可以对此喋喋不休,但你似乎至少已经知道最好的答案。将数据转换为数组。“查询”文档的唯一方法基本上包括在服务器上操作文档(每次文档处理),以强制将这些“命名键”转换为数组。
在最近的MongoDB版本中有更多的现代方法,这意味着你不必使用$where
,但是仍然有一个主要的警告,这仍然是错误的形式,你只是“不能使用索引”来加速查询结果。
在你的问题的基本解决:
如果您想根据分支机构员工雇佣日期的条件“查找文件”,那么您可以使用带有$where
的表达式,如下所示:
db.bank_branches.find(
function() {
return Object.keys(this.branch_employees).some(e =>
e.hire_date => new Date("2016-01-01") && e.hire_date < new Date("2017-01-01")
)
}
)
我们使用JavaScript评估的主要原因是,您需要以查询DSL无法表达的方式“遍历文档的键”,因此需要针对每个文档评估一个条件,而不是使用索引。
对于自MongoDB 3.4.4以来的最新版本,您可以使用$object tToArray
来实现与Object. key()
几乎相同的目的,并将其用作$redact
聚合管道阶段中表达式的一部分:
db.bank_branches.aggregate([
{ "$redact": {
"$cond": {
"if": {
"$anyElementTrue": {
"$map": {
"input": { "$objectToArray": "$branch_employees" },
"in": {
"$and": [
{ "$gte": [ "$$this.hire_date", new Date("2016-01-01") ] },
{ "$lt": [ "$$this.hire_date", new Date("2017-01-01") ] }
]
}
}
}
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
$redact
本质上评估一个条件,并通过$$KEEP
返回或通过$$PRUNE
根据条件的布尔结果“删除”文档。这类似于$where
,只是它是一个本机运算符,而不是使用解释的JavaScript,本质上是“整体”查询条件,而不是$where
,后者在技术上只是另一个查询参数,可以与其他条件一起使用。
从MongoDB 3.6中,我们得到$expr
,它允许它缩短,甚至用作与$where
相同的“附加参数”语法,除非使用本机编码运算符:
db.bank_branches.find({
"$expr": {
"$anyElementTrue": {
"$map": {
"input": { "$objectToArray": "$branch_employees" },
"in": {
"$and": [
{ "$gte": [ "$$this.hire_date", new Date("2016-01-01") ] },
{ "$lt": [ "$$this.hire_date", new Date("2017-01-01") ] }
]
}
}
}
}
})
但它们基本上仍然很可怕,因为您所能做的就是“扫描整个集合”以获得结果。所以更好的“数组”语法是:
db.bank_branches.find({
"branch_employees": {
"$elemMatch": {
"hire_date": { "$gte": new Date("2016-01-01"), "$lte": new Date("2017-01-01") }
}
}
})
它当然可以使用索引,因为"branch_employees.hire_date"
的路径是一致的,并且不使用“命名键”作为所需属性的中间路径的一部分。这是您想要这个结构的主要原因
为了实际获得该“数组形状”的文档,那么我们应该通过查询的构造方式得到一些指示。
因此,在第一个选项中,如果写入“新集合”是您的一个选项,那么在现代版本中,您应该能够简单地在服务器本身上完成整个转换:
db.bank_branches.aggregate([
{ "$project": {
"branch": 1,
"branch_employees": {
"$map": {
"input": { "$objectToArray": "$branch_employees" },
"in": {
"$arrayToObject": {
"$concatArrays": [
[{ "k": "id", "v": "$$this.k" }],
{ "$objectToArray": "$$this.v" }
]
}
}
}
}
}},
{ "$out": "new_branches" }
])
或者,如果您没有可用的现代运算符或根本无法写入新集合,则基本上循环集合并写回属性的新数据:
var ops = [];
db.bank_branches.find({ "branch_employees.0": { "$exists": false } }).forEach( doc => {
ops.push({
"updateOne": {
"filter": { "_id": doc._id },
"update": {
"$set": {
"branch_employees": Object.keys(doc.branch_employees).map(id =>
Object.assign({ id },doc.branch_employees[id])
)
}
}
}
});
if ( ops.length > 1000 ) {
db.bank_branches.bulkWrite(ops);
ops = [];
}
})
if ( ops.length > 0 ) {
db.bank_branches.bulkWrite(ops);
ops = [];
}
两者本质上都使用相同的技术,即获取对象子键的值并将其与底层对象合并,作为作为“数组”返回的元素。
N.在$where
表达式中运行的代码在JavaScript中并在服务器上运行,因此它仍然是一个“语言无关的解决方案”,其中要评估的“JavaScript”实际上是以其他语言的“字符串”提交的。
其他表达式基本上分解为所选语言中的BSON表示。Python和Ruby在语法方面与JavaScript几乎相同,并且相同的BSON约定适用于任何地方。
这里给出的其他例程用于数据的“转换”,作为“一次性”操作,它应该始终足以在shell的JavaScript执行环境中运行。
因此,没有“需要”使用JavaScript($where
表达式除外),但此处以通用格式给出了示例,每个人都可以在MongoDB安装提供的shell中运行。