$ lookup на Двойном Вложенном Иностранном Поле - PullRequest
0 голосов
/ 03 мая 2018

У меня есть 2 коллекции:

Пользователь

{
    id: 1,
    name: "Michael", 
    starred: [1, 2]
}

Школа

{
    id: 1,
    name: "Uni", 
    faculties: [{
        id:1000, 
        name: "faculty1", 
        subjects: [
            {id: 1, name: "sub1"},
            {id: 2, name: "sub2"},
            {id: 3, name: "sub3"}
        ]
    }]
}

Теперь в моей коллекции пользователей я бы хотел найти и собрать каждый предметный объект с идентификатором, найденным в starred. то есть. starred: [1,2] содержит id предметов, которые я хочу.

Таким образом, конечный результат должен вернуть

[{id: 1, name: sub1},{id: 2, name: sub2}]

Я сейчас работаю с конвейером агрегации

{$match: {name: 'Michael'}},
{$unwind: "$faculties"},
{$unwind: "$faculties.subjects"},
{$lookup:
  {
     from: 'schools',
     localField: 'starred',
     foreignField: 'faculties.subjects.id',
     as: 'starredSubjects'
   }
},
{$project: {starredSubjects: 1}}

но раскрутка не работает (я думаю, потому что я пытаюсь раскрутить чужую коллекцию, а не локальную (то есть Users). Также foreignField: 'faculties.subjects.id ничего не возвращает. Что мне не хватает?

(sidenote: тестирование отлично подходит для плагина MongoExplorer webstorm).

1 Ответ

0 голосов
/ 03 мая 2018

Это действительно не очень хорошая структура для работы, и для этого есть очень веские причины. Таким образом, выполнение $lookup здесь не простая задача, поскольку существуют различные последствия использования «вложенных массивов»

Вы в основном хотите либо

db.users.aggregate([
   { "$match": { "name": "Michael" } },
   { "$lookup": {
     "from": "schools",
     "localField": "starred",
     "foreignField": "faculties.subjects.id",
     "as": "subjects"
   }},
   { "$addFields": {
     "subjects": {
       "$filter": {
         "input": {
           "$reduce": {
             "input": {
               "$reduce": {
                 "input": "$subjects.faculties.subjects",
                 "initialValue": [],
                 "in": { "$concatArrays": [ "$$value", "$$this" ] }
               }
             },
             "initialValue": [],
             "in": { "$concatArrays": [ "$$value", "$$this" ] }
           }
         },
         "cond": { "$in": ["$$this.id", "$starred"] }
       }
     }
   }}
])

Или с MongoDB 3.6 или выше, может быть вместо:

db.users.aggregate([
  { "$match": { "name": "Michael" } },
  { "$lookup": {
    "from": "schools",
    "let": { "starred": "$starred" },
    "pipeline": [
      { "$match": {
        "$expr": {
          "$setIsSubset": [ 
            "$$starred",
            { "$reduce": {
              "input": "$faculties.subjects.id",
              "initialValue": [],
              "in": { "$concatArrays": [ "$$value", "$$this" ] }
            }}
          ]
        }
      }},
      { "$project": {
        "_id": 0,
        "subjects": {
          "$filter": {
            "input": {
              "$reduce": {
                "input":  "$faculties.subjects",
                "initialValue": [],
                "in": { "$concatArrays": [ "$$value", "$$this" ] }
              }
            },
            "cond": { "$in": [ "$$this.id", "$$starred" ] }
          }
        }
      }},
      { "$unwind": "$subjects" },
      { "$replaceRoot": { "newRoot": "$subjects" } }
    ],
    "as": "subjects"
  }}
])

Оба подхода в основном полагаются на $reduce и $concatArrays для того, чтобы «сгладить» содержимое «вложенного массива» в форму, которую можно использовать для сравнения. Основное различие между ними заключается в том, что до MongoDB 3.6 вы, по сути, извлекали все «возможные» совпадения из документа, прежде чем можно было что-либо делать с «фильтрацией» записей внутреннего массива только по тем, которые совпадают.

Если у вас не меньше MongoDB 3.4 с операторами $reduce и $in, то вы, по сути, прибегаете к $unwind:

db.users.aggregate([
   { "$match": { "name": "Michael" } },
   { "$lookup": {
     "from": "schools",
     "localField": "starred",
     "foreignField": "faculties.subjects.id",
     "as": "subjects"
   }},
   { "$unwind": "$subjects" },
   { "$unwind": "$subjects.faculties" },
   { "$unwind": "$subjects.faculties.subjects" },
   { "$redact": {
      "$cond": {
        "if": {
          "$setIsSubset": [
            ["$subjects.faculties.subjects.id"],
            "$starred"
          ]
        },
        "then": "$$KEEP",
        "else": "$$PRUNE"
      }
   }},
   { "$group": {
      "_id": "$_id",
      "id": { "$first": "$id" },
      "name": { "$first": "$name" },
      "starred": { "$first": "$starred" },
      "subjects": { "$push": "$subjects.faculties.subjects" }
   }}
])

Использование, конечно, этапа $redact для фильтрации логического сравнения, поскольку для сравнения есть только $expr с MongoDB 3.6 и $setIsSubset в массив "starred".

Тогда, конечно, из-за всех операций $unwind обычно требуется $group, чтобы преобразовать массив.

Или иначе сделать $lookup с другого направления:

db.schools.aggregate([
  { "$unwind": "$faculties" },
  { "$unwind": "$faculties.subjects" },
  { "$lookup": {
    "from": "users",
    "localField": "faculties.subjects.id",
    "foreignField": "starred",
    "as": "users"
  }},
  { "$unwind": "$users" },
  { "$match": { "users.name": "Michael" } },
  { "$group": {
    "_id": "$users._id",
    "id": { "$first": "$users.id" },
    "name": { "$first": "$users.name" },
    "starred": { "$first": "$users.starred" },
    "subjects": {
      "$push": "$faculties.subjects"
    }    
  }}
])

Последняя форма на самом деле не идеальна, так как вы не фильтруете «пользователей» до тех пор, пока не будет сделано $lookup (или, технически говоря, «во время» $lookup действительно ). Но, во всяком случае, в первую очередь он должен работать со всей коллекцией "школ".

Все формы возвращают одинаковый вывод:

{
    "_id" : ObjectId("5aea649526a94676bb981df4"),
    "id" : 1,
    "name" : "Michael",
    "starred" : [
            1,
            2
    ],
    "subjects" : [
        {
                "id" : 1,
                "name" : "sub1"
        },
        {
                "id" : 2,
                "name" : "sub2"
        }

    ]
}

Если у вас есть только детали из внутреннего массива "subjects" из соответствующего документа, которые фактически соответствуют значениям "starred" для текущего пользователя.


Все это говорит о том, что не очень хорошая идея "вкладывать массивы" в MongoDB. До MongoDB 3.6 вы не можете даже атомарные обновления «вложенных массивов» и даже с изменениями, которые позволяют это, все равно «сложно» в лучшем случае выполнять любые операции запроса, и особенно те, которые связаны с объединениями и фильтрацией.

Распространенной ошибкой новичка является структурирование «вложенных массивов», поскольку вы, похоже, думаете, что «лучше» организовываете вещи. Но на самом деле это скорее «анти-паттерн», и вы действительно должны рассмотреть «более плоскую» структуру, такую ​​как:

{
    "_id" : ObjectId("5aea651326a94676bb981df5"),
    "id" : 1,
    "name" : "Uni",
    "subjects" : [
        {
                "id" : 1,
                "name" : "sub1",
                "facultyId": 1000,
                "facultyName": "faculty1"
        },
        {
                "id" : 2,
                "name" : "sub2",
                "facultyId": 1000,
                "facultyName": "faculty1"

        },
        {
                "id" : 3,
                "name" : "sub3",
                "facultyId": 1000,
                "facultyName": "faculty1"

        }
    ]
}

Что "намного" легче работать и, конечно, выполнять "соединения" там, где это необходимо.

...