Mongoose 反向查找和删除

Mongoose reverse lookup and delete

我正在尝试对我的 collections 之一中的引用进行健康检查。所以要查看引用的对象是否仍然存在,如果不存在,我想删除 array

中的那个 _id

我还没有找到任何相关信息,所以我的想法是获得 $lookup

reversed 结果

是否可以在 MongoDB 中获得查找的反向结果?

这是一个集合示例,它的 taskList 引用了 tasks 集合。

现在我想删除 tasks 集合中没有现有结果的所有 ID。

我现在是如何解决这个问题的:

  1. taskList
  2. 获取所有 ID
  3. 对它们中的每一个发送查询以查看是否与任务集合不匹配
  4. 发送查询以从数组中提取空引用

Afaik 除了您描述的以外没有其他方法可以达到预期的结果,但是您可以大大简化第二步以找到不匹配的项目。事实上,它是 taskList-id 和现有 task-id 之间的设置差异。

因此您可以使用 $setDifference-operator 来计算差异:

db.tasks.aggregate([
  {
    $group: {
      _id: "null",
      ids: {
        "$addToSet": "$_id"
      }
    }
  },
  {
    $project: {
      nonMatchingTaskIds: {
        $setDifference: [
          [
            "taskId1",
            "taskId2",
            "taskId7",
            "taskId8"
          ],
          "$ids"
        ]
      }
    }
  }
])

假设您的 tasks 集合包含 taskId1task2(和其他文档),但不包含 taskId7taskId8,查询将导致nonMatchingTaskIds 包含 taskId7taskId8

这是 mongoplayground 上的一个例子:https://mongoplayground.net/p/75BpiGBJi3Q

我认为这可以满足您的需求,即使您的 collection 很大也可以。

但这不是您可以在 $merge 阶段之后对任务列表执行的更新(如果匹配 _id 替换)(需要 MongoDB >= 4.4)或者您可以执行 $out 阶段到另一个 collection,并替换任务列表 collection。

Test code here

数据在

db={
  "tasklists": [
    {
      "_id": 1,
      "tasklist": [
        1,
        2,
        3,
        4
      ]
    },
    {
      "_id": 2,
      "tasklist": [
        5,
        6,
        7
      ]
    }
  ],
  "tasks": [
    {
      "_id": 1
    },
    {
      "_id": 2
    },
    {
      "_id": 3
    },
    {
      "_id": 5
    }
  ]
}
db.tasklists.aggregate([
  {
    "$lookup": {
      "from": "tasks",
      "let": {
        "tasklist": "$tasklist"
      },
      "pipeline": [
        {
          "$match": {
            "$expr": {
              "$in": [
                "$_id",
                "$$tasklist"
              ]
            }
          }
        }
      ],
      "as": "valid"
    }
  },
  {
    "$addFields": {
      "valid": {
        "$map": {
          "input": "$valid",
          "as": "v",
          "in": "$$v._id"
        }
      }
    }
  },
  {
    "$addFields": {
      "tasklist": {
        "$filter": {
          "input": "$tasklist",
          "as": "t",
          "cond": {
            "$in": [
              "$$t",
              "$valid"
            ]
          }
        }
      }
    }
  },
  {
    "$unset": [
      "valid"
    ]
  }
])

结果(任务 4、6、7 未在任务 collection 中找到并已删除)

[
  {
    "_id": 1,
    "tasklist": [
      1,
      2,
      3
    ]
  },
  {
    "_id": 2,
    "tasklist": [
      5
    ]
  }
]

编辑

如果你想使用索引来做 $lookup 你可以试试这个

Test code here

任务在 _id 上有索引,所以不需要做一个,如果你不加入 _id 就做一个。

db.tasklists.aggregate([
  {
    "$unwind": {
      "path": "$tasklist"
    }
  },
  {
    "$lookup": {
      "from": "tasks",
      "localField": "tasklist",
      "foreignField": "_id",
      "as": "joined"
    }
  },
  {
    "$match": {
      "$expr": {
        "$gt": [
          {
            "$size": "$joined"
          },
          0
        ]
      }
    }
  },
  {
    "$unset": [
      "joined"
    ]
  },
  {
    "$group": {
      "_id": "$_id",
      "tasklist": {
        "$push": "$tasklist"
      },
      "afield": {
        "$first": "$afield"
      }
    }
  }
])

之后,您可以使用替换选项执行 $out$merge。 但是,如果发生这种情况,两者都会丢失更新的数据。

唯一的解决方案(如果这是一个问题)$merge 使用管道, 您还需要在带有初始任务列表的额外数组上方的管道中保留,因此您删除有效的,以拥有无效的,然后与管道合并以过滤数组,并删除那些无效的。 (这是安全的,不会丢失数据)

我认为最好的方法不是做所有这些,而是​​在任务列表上有一个索引(多键索引),当一个 _id 从任务中删除时,从 tasklist.With 索引它的数组中删除 _id会很快,所以你不需要检查无效的_ids。

所以我现在来做的是几个步骤的方法。 这是相当快的,但是从集合中收集的 taskIds 目前比我想象的集合的总量要小得多,一旦我得到那么多引用,使用 eol 提到的 $setDifference 运算符会更快。

let taskIdsInSets = []
    // Get all referenced task ids
    const result = await this.setSchema.aggregate([
      {
        '$project': {
          'taskList': 1
        }
      }
    ])

    // Map all elements in one row
    result.forEach(set => taskIdsInSets.push(...set.taskList.map(x=> x.toString())))

    // Delete duplicates of taskIds here
    taskIdsInSets.filter((item, index) => taskIdsInSets.indexOf(item) != index)

    // Get the existing task ids that are referenced in a Set
    const result2 = await this.taskSchema.aggregate([
      {
        '$match': {
          '_id': {
            '$in': [...taskIdsInSets.map(x => Types.ObjectId(x.toString()))]
          }
        }
      }, {
        '$project': {
          '_id': 1
        }
      }
    ])

    let existingIdsInTasks = []
    // Getting ids from result2 Object into
    result2.forEach(set => existingIdsInTasks.push(set._id.toString()))

    // Filtering out the ids that don't actually exist
    let nonExistingTaskIds = taskIdsInSets.filter(x => existingIdsInTasks.indexOf(x) === -1);

    // Deleting the ids that don't actually exist but are in Sets
    const finalResult = await this.setSchema.updateMany(
      {
        $pullAll: {
          taskList: [...nonExistingTaskIds.map(x => Types.ObjectId(x.toString()))]
        }
      })
    console.log(finalResult)
    return finalResult // returns the information how much got changed. unfortunately in mongoose there isn't the option to use findAndModify with `{new:true}` or atleast I didn't manage to make it work.

出于某种原因,数据库 returns 既不匹配 Mongo ObjectId 也不匹配字符串,所以我必须在那里做一些转换。