Фильтровать по присоединенному поддокументу - PullRequest
0 голосов
/ 03 марта 2019

Я пытаюсь отфильтровать документ по вложенному документу, указанному свойством.Предположим, что я уже создал модели для каждой схемы.Упрощенные схемы следующие:

const store = new Schema({
    name: { type: String }
})

const price = new Schema({
    price: { type: Number },
    store: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Store'
    },
})

const product = new Schema({
    name: {type: String},
    prices: [{
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Price'
    }] 
})
/* 
Notation: 
lowercase for schemas: product
uppercase for models: Product
*/

В качестве первого подхода я попробовал:

Product.find({'prices.store':storeId}).populate('prices')

, но это не работает как фильтрация по поддокументусвойство не поддерживается на мангусте.

Мой текущий подход использует структуру агрегации.Вот как выглядит агрегация:

{
  $unwind: '$prices'
},
{
  $lookup: {
    from: 'prices',
    localField: 'prices',
    foreignField: '_id',
    as: 'prices'
  }
},
{
  $unwind: '$prices'
},
{
  $lookup: {
    from: 'stores',
    localField: 'prices.store',
    foreignField: '_id',
    as: 'prices.store'
  }
}, // populate
{
  $match: {
    'prices.store._id': new mongoose.Types.ObjectId(storeId)
  }
}, // filter by store id
{ $group: { _id: '$id', doc: { $first: '$$ROOT' } } },
{ $replaceRoot: { newRoot: '$doc' } }
// Error occurs in $group & $replaceRoot

Например, перед двумя последними этапами, если сохраняемая запись имеет вид:

{
    name: 'Milk', 
    prices: [
        {store: 1, price: 3.2}, 
        {store: 2, price: 4.0}
    ]
}

, то агрегация вернулась: (обратите внимание, что продуктто же самое, но каждая цена отображается в разных результатах)

[ 
    {
        id: 4,
        name: 'Milk', 
        prices: {
           id: 10,
           store: { _id: 1, name : 'Walmart' }, 
           price: 3.2
        }
    },
    {
        id: 4,
        name: 'Milk', 
        prices: {
           id: 11,
           store: { _id: 2, name : 'CVS' }, 
           price: 4.0
        },
    }
]

Чтобы решить эту проблему, я добавил последнюю часть:

{ $group: { _id: '$id', doc: { $first: '$$ROOT' } } },
{ $replaceRoot: { newRoot: '$doc' } }

Но эта последняя часть возвращает только следующее:

{
    id: 4,
    name: 'Milk', 
    prices: {
        id: 10,
        store: { _id: 1, name : 'Walmart' }, 
        price: 3.2
    }
}

Теперь prices - это объект, он должен быть массивом и содержать все цены (в данном случае 2).

Вопрос

Как вернуть все цены (в виде массива) с полем магазина, заполненным и отфильтрованным по storeId?

Ожидаемый результат:

{
    id: 4,
    name: 'Milk', 
    prices: [
    {
        id: 10,
        store: { _id: 1, name : 'Walmart' }, 
        price: 3.2
    },
    {
        id: 11,
        store: { _id: 2, name : 'CVS' }, 
        price: 4.0
    }]
}

РЕДАКТИРОВАТЬ

Я хочу отфильтровать товары, которые содержат цены в данном магазине.Он должен вернуть товар с его ценами, все они.

1 Ответ

0 голосов
/ 03 марта 2019

Я не совсем уверен, что ваш существующий конвейер является наиболее оптимальным, но без примерных данных, из которых трудно работать, трудно сказать иначе.Так что продолжайте работать с того, что у вас есть:

Использование $ unwind

var pipeline =  [
    // { $unwind: '$prices' }, // note: should not need this past MongoDB 3.0
    { $lookup: {
        from: 'prices',
        localField: 'prices',
        foreignField: '_id',
        as: 'prices'
     }},
     { $unwind: '$prices' },
     { $lookup: {
        from: 'stores',
        localField: 'prices.store',
        foreignField: '_id',
        as: 'prices.store'
      }},
      // Changes from here
      { $unwind: '$prices.store' },
      { $match: {'prices.store._id': mongoose.Types.ObjectId(storeId) } },
      { $group: {
        _id: '$_id',
        name: { $first: '$name' },
        prices: { $push: '$prices' }
      }}
];

Точки там начинаются с:

  • Initial $unwind - не требуется.Только в очень ранних выпусках MongoDB 3.0 это когда-либо требовалось $unwind массив значений перед использованием $lookup для этих значений.

  • $unwind после $lookup - всегда требуется, если вы ожидаете совпадения с "единичным" объектом, поскольку $lookup всегда возвращает массив.

  • $match после $unwind - Фактически «оптимизация» для обработки конвейера и фактически требование для «фильтрации» .Без $unwind это просто подтверждение того, что «что-то есть» , но не соответствующие элементы не будут удалены.

  • $push in $group - это фактическая часть, которая перестраивает массив "prices".

Ключевым моментом, который вы в основном упускали, было использование $first для содержимого "всего документа".Вы действительно никогда не хотите этого, и даже если вы хотите больше, чем просто "name", вы всегда хотите $push "prices".

На самом деле вы, вероятно, хотитебольше полей, чем просто name из исходного документа, но на самом деле вам следует вместо этого использовать следующую форму:

Expressive $ lookup

Альтернатива доступна в большинстве современных выпусков MongoDB, начиная с MongoDB3.6, который, честно говоря, вы должны использовать как минимум:

var pipeline =  [
    { $lookup: {
        from: 'prices',
        let: { prices: '$prices' },
        pipeline: [
          { $match: {
            store: mongoose.Types.ObjectId(storeId),
            $expr: { $in: [ '$_id', '$$prices' ] }
          }},
          { $lookup: {
            from: 'stores',
            let: { store: '$store' },
            pipeline: [
              { $match: { $expr: { $eq: [ '$_id', '$$store' ] } }
            ],
            as: 'store'
          }},
          { $unwind: '$store' }
        ],
        as: 'prices'
    }},
    // remove results with no matching prices
    { $match: { 'prices.0': { $exists: true } } }
];         

Итак, первое, что нужно заметить, это «внешний» pipeline, на самом деле это всего лишь одна $lookup стадия, поскольку все, что ему действительно нужно, это «присоединиться» к коллекции prices.С точки зрения присоединения к вашей исходной коллекции это также верно, поскольку дополнительный $lookup в приведенном выше примере фактически связан с prices с другой коллекцией.

Это точночто делает эта новая форма, поэтому вместо использования $unwind в результирующем массиве, а затем после объединения, только соответствующие элементы для «цен» затем «соединяются» сколлекция "stores" и до возвращаются в массив.Конечно, поскольку существует отношение «один к одному» с «магазином», на самом деле это будет $unwind.

Короче говоря, на выходе этого просто будет оригинальный документ с"prices" массив внутри него.Таким образом, нет необходимости перестраивать через $group и не путать то, что вы используете $first и что вы $push.


ПРИМЕЧАНИЕ : я более чем подозреваю ваше утверждение "хранилища фильтров" и пытаюсь сопоставить поле store, представленное в "prices"коллекция.Вопрос показывает ожидаемый результат из двух разных магазинов, даже если вы укажете совпадение равенства.

Если я что-то подозреваю, вы могли бы означать «список магазинов» , чтовместо этого было бы больше похоже на:

store: { $in: storeList.map(store => mongoose.Types.ObjectId(store)) }

Вот как вы будете работать с «списком строк» ​​ в обоих случаях, используя $in для сопоставлениядля «списка» и Array.map() для работы с предоставленным списком и возврата каждого в виде ObjectId() значений.

TIP : с mongoose вы используете «модель» вместо того, чтобы работатьс именами коллекций, и фактические имена коллекций MongoDB обычно являются множественным числом от имени модели, которое вы зарегистрировали.

Таким образом, вам не нужно «жестко» кодировать фактические имена коллекций для $lookup, просто используйте:

   Model.collection.name

.collection.name является доступным свойством для всех моделей и может избавить вас от необходимости помнить, как на самом деле назвать коллекцию для $lookup.Он также защищает вас от любых изменений регистрации mongoose.model() экземпляра таким образом, чтобы изменить имя хранимой коллекции с помощью MongoDB.


Полная демонстрация

Ниже приведен автономный списокдемонстрируя оба подхода как работу и то, как они дают одинаковые результаты:

const { Schema, Types: { ObjectId } } = mongoose = require('mongoose');

const uri = 'mongodb://localhost:27017/shopping';
const opts = { useNewUrlParser: true };

mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);

const storeSchema = new Schema({
  name: { type: String }
});

const priceSchema = new Schema({
  price: { type: Number },
  store: { type: Schema.Types.ObjectId, ref: 'Store' }
});

const productSchema = new Schema({
  name: { type: String },
  prices: [{ type: Schema.Types.ObjectId, ref: 'Price' }]
});

const Store = mongoose.model('Store', storeSchema);
const Price = mongoose.model('Price', priceSchema);
const Product = mongoose.model('Product', productSchema);

const log = data => console.log(JSON.stringify(data, undefined, 2));

(async function() {

  try {

    const conn = await mongoose.connect(uri, opts);

    // Clean data
    await Promise.all(
      Object.entries(conn.models).map(([k, m]) => m.deleteMany())
    );

    // Insert working data

    let [StoreA, StoreB, StoreC] = await Store.insertMany(
      ["StoreA", "StoreB", "StoreC"].map(name => ({ name }))
    );


    let [PriceA, PriceB, PriceC, PriceD, PriceE, PriceF]
      = await Price.insertMany(
          [[StoreA,1],[StoreB,2],[StoreA,3],[StoreC,4],[StoreB,5],[StoreC,6]]
            .map(([store, price]) => ({ price, store }))
        );


    let [Milk, Cheese, Bread] = await Product.insertMany(
      [
        { name: 'Milk', prices: [PriceA, PriceB] },
        { name: 'Cheese', prices: [PriceC, PriceD] },
        { name: 'Bread', prices: [PriceE, PriceF] }
      ]
    );


    // Test 1
    {
      log("Single Store - expressive")
      const pipeline = [
        { '$lookup': {
          'from': Price.collection.name,
          'let': { prices: '$prices' },
          'pipeline': [
            { '$match': {
              'store': ObjectId(StoreA._id),  // demo - it's already an ObjectId
              '$expr': { '$in': [ '$_id', '$$prices' ] }
            }},
            { '$lookup': {
              'from': Store.collection.name,
              'let': { store: '$store' },
              'pipeline': [
                { '$match': { '$expr': { '$eq': [ '$_id', '$$store' ] } } }
              ],
              'as': 'store'
            }},
            { '$unwind': '$store' }
          ],
          as: 'prices'
        }},
        { '$match': { 'prices.0': { '$exists': true } } }
      ];

      let result = await Product.aggregate(pipeline);
      log(result);
    }

    // Test 2
    {
      log("Dual Store - expressive");
      const pipeline = [
        { '$lookup': {
          'from': Price.collection.name,
          'let': { prices: '$prices' },
          'pipeline': [
            { '$match': {
              'store': { '$in': [StoreA._id, StoreB._id] },
              '$expr': { '$in': [ '$_id', '$$prices' ] }
            }},
            { '$lookup': {
              'from': Store.collection.name,
              'let': { store: '$store' },
              'pipeline': [
                { '$match': { '$expr': { '$eq': [ '$_id', '$$store' ] } } }
              ],
              'as': 'store'
            }},
            { '$unwind': '$store' }
          ],
          as: 'prices'
        }},
        { '$match': { 'prices.0': { '$exists': true } } }
      ];

      let result = await Product.aggregate(pipeline);
      log(result);
    }

    // Test 3
    {
      log("Single Store - legacy");
      const pipeline = [
        { '$lookup': {
          'from': Price.collection.name,
          'localField': 'prices',
          'foreignField': '_id',
          'as': 'prices'
        }},
        { '$unwind': '$prices' },
        // Alternately $match can be done here
        // { '$match': { 'prices.store': StoreA._id } },

        { '$lookup': {
          'from': Store.collection.name,
          'localField': 'prices.store',
          'foreignField': '_id',
          'as': 'prices.store'
        }},
        { '$unwind': '$prices.store' },
        { '$match': { 'prices.store._id': StoreA._id } },
        { '$group': {
          '_id': '$_id',
          'name': { '$first': '$name' },
          'prices': { '$push': '$prices' }
        }}
      ];

      let result = await Product.aggregate(pipeline);
      log(result);
    }

    // Test 4
    {
      log("Dual Store - legacy");
      const pipeline = [
        { '$lookup': {
          'from': Price.collection.name,
          'localField': 'prices',
          'foreignField': '_id',
          'as': 'prices'
        }},
        { '$unwind': '$prices' },
        // Alternately $match can be done here
        { '$match': { 'prices.store': { '$in': [StoreA._id, StoreB._id] } } },

        { '$lookup': {
          'from': Store.collection.name,
          'localField': 'prices.store',
          'foreignField': '_id',
          'as': 'prices.store'
        }},
        { '$unwind': '$prices.store' },
        //{ '$match': { 'prices.store._id': { '$in': [StoreA._id, StoreB._id] } } },
        { '$group': {
          '_id': '$_id',
          'name': { '$first': '$name' },
          'prices': { '$push': '$prices' }
        }}
      ];

      let result = await Product.aggregate(pipeline);
      log(result);
    }

  } catch(e) {
    console.error(e);
  } finally {
    mongoose.disconnect();
  }


})()

, который выдает результат:

Mongoose: stores.deleteMany({}, {})
Mongoose: prices.deleteMany({}, {})
Mongoose: products.deleteMany({}, {})
Mongoose: stores.insertMany([ { _id: 5c7c79bcc78675135c09f54b, name: 'StoreA', __v: 0 }, { _id: 5c7c79bcc78675135c09f54c, name: 'StoreB', __v: 0 }, { _id: 5c7c79bcc78675135c09f54d, name: 'StoreC', __v: 0 } ], {})
Mongoose: prices.insertMany([ { _id: 5c7c79bcc78675135c09f54e, price: 1, store: 5c7c79bcc78675135c09f54b, __v: 0 }, { _id: 5c7c79bcc78675135c09f54f, price: 2, store: 5c7c79bcc78675135c09f54c, __v: 0 }, { _id: 5c7c79bcc78675135c09f550, price: 3, store: 5c7c79bcc78675135c09f54b, __v: 0 }, { _id: 5c7c79bcc78675135c09f551, price: 4, store: 5c7c79bcc78675135c09f54d, __v: 0 }, { _id: 5c7c79bcc78675135c09f552, price: 5, store: 5c7c79bcc78675135c09f54c, __v: 0 }, { _id: 5c7c79bcc78675135c09f553, price: 6, store: 5c7c79bcc78675135c09f54d, __v: 0 } ], {})
Mongoose: products.insertMany([ { prices: [ 5c7c79bcc78675135c09f54e, 5c7c79bcc78675135c09f54f ], _id: 5c7c79bcc78675135c09f554, name: 'Milk', __v: 0 }, { prices: [ 5c7c79bcc78675135c09f550, 5c7c79bcc78675135c09f551 ], _id: 5c7c79bcc78675135c09f555, name: 'Cheese', __v: 0 }, { prices: [ 5c7c79bcc78675135c09f552, 5c7c79bcc78675135c09f553 ], _id: 5c7c79bcc78675135c09f556, name: 'Bread', __v: 0 } ], {})
"Single Store - expressive"
Mongoose: products.aggregate([ { '$lookup': { from: 'prices', let: { prices: '$prices' }, pipeline: [ { '$match': { store: 5c7c79bcc78675135c09f54b, '$expr': { '$in': [ '$_id', '$$prices' ] } } }, { '$lookup': { from: 'stores', let: { store: '$store' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$store' ] } } } ], as: 'store' } }, { '$unwind': '$store' } ], as: 'prices' } }, { '$match': { 'prices.0': { '$exists': true } } } ], {})
[
  {
    "_id": "5c7c79bcc78675135c09f554",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f54e",
        "price": 1,
        "store": {
          "_id": "5c7c79bcc78675135c09f54b",
          "name": "StoreA",
          "__v": 0
        },
        "__v": 0
      }
    ],
    "name": "Milk",
    "__v": 0
  },
  {
    "_id": "5c7c79bcc78675135c09f555",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f550",
        "price": 3,
        "store": {
          "_id": "5c7c79bcc78675135c09f54b",
          "name": "StoreA",
          "__v": 0
        },
        "__v": 0
      }
    ],
    "name": "Cheese",
    "__v": 0
  }
]
"Dual Store - expressive"
Mongoose: products.aggregate([ { '$lookup': { from: 'prices', let: { prices: '$prices' }, pipeline: [ { '$match': { store: { '$in': [ 5c7c79bcc78675135c09f54b, 5c7c79bcc78675135c09f54c ] }, '$expr': { '$in': [ '$_id', '$$prices' ] } } }, { '$lookup': { from: 'stores', let: { store: '$store' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$store' ] } } } ], as: 'store' } }, { '$unwind': '$store' } ], as: 'prices' } }, { '$match': { 'prices.0': { '$exists': true } } } ], {})
[
  {
    "_id": "5c7c79bcc78675135c09f554",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f54e",
        "price": 1,
        "store": {
          "_id": "5c7c79bcc78675135c09f54b",
          "name": "StoreA",
          "__v": 0
        },
        "__v": 0
      },
      {
        "_id": "5c7c79bcc78675135c09f54f",
        "price": 2,
        "store": {
          "_id": "5c7c79bcc78675135c09f54c",
          "name": "StoreB",
          "__v": 0
        },
        "__v": 0
      }
    ],
    "name": "Milk",
    "__v": 0
  },
  {
    "_id": "5c7c79bcc78675135c09f555",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f550",
        "price": 3,
        "store": {
          "_id": "5c7c79bcc78675135c09f54b",
          "name": "StoreA",
          "__v": 0
        },
        "__v": 0
      }
    ],
    "name": "Cheese",
    "__v": 0
  },
  {
    "_id": "5c7c79bcc78675135c09f556",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f552",
        "price": 5,
        "store": {
          "_id": "5c7c79bcc78675135c09f54c",
          "name": "StoreB",
          "__v": 0
        },
        "__v": 0
      }
    ],
    "name": "Bread",
    "__v": 0
  }
]
"Single Store - legacy"
Mongoose: products.aggregate([ { '$lookup': { from: 'prices', localField: 'prices', foreignField: '_id', as: 'prices' } }, { '$unwind': '$prices' }, { '$lookup': { from: 'stores', localField: 'prices.store', foreignField: '_id', as: 'prices.store' } }, { '$unwind': '$prices.store' }, { '$match': { 'prices.store._id': 5c7c79bcc78675135c09f54b } }, { '$group': { _id: '$_id', name: { '$first': '$name' }, prices: { '$push': '$prices' } } } ], {})
[
  {
    "_id": "5c7c79bcc78675135c09f555",
    "name": "Cheese",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f550",
        "price": 3,
        "store": {
          "_id": "5c7c79bcc78675135c09f54b",
          "name": "StoreA",
          "__v": 0
        },
        "__v": 0
      }
    ]
  },
  {
    "_id": "5c7c79bcc78675135c09f554",
    "name": "Milk",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f54e",
        "price": 1,
        "store": {
          "_id": "5c7c79bcc78675135c09f54b",
          "name": "StoreA",
          "__v": 0
        },
        "__v": 0
      }
    ]
  }
]
"Dual Store - legacy"
Mongoose: products.aggregate([ { '$lookup': { from: 'prices', localField: 'prices', foreignField: '_id', as: 'prices' } }, { '$unwind': '$prices' }, { '$match': { 'prices.store': { '$in': [ 5c7c79bcc78675135c09f54b, 5c7c79bcc78675135c09f54c ] } } }, { '$lookup': { from: 'stores', localField: 'prices.store', foreignField: '_id', as: 'prices.store' } }, { '$unwind': '$prices.store' }, { '$group': { _id: '$_id', name: { '$first': '$name' }, prices: { '$push': '$prices' } } } ], {})
[
  {
    "_id": "5c7c79bcc78675135c09f555",
    "name": "Cheese",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f550",
        "price": 3,
        "store": {
          "_id": "5c7c79bcc78675135c09f54b",
          "name": "StoreA",
          "__v": 0
        },
        "__v": 0
      }
    ]
  },
  {
    "_id": "5c7c79bcc78675135c09f556",
    "name": "Bread",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f552",
        "price": 5,
        "store": {
          "_id": "5c7c79bcc78675135c09f54c",
          "name": "StoreB",
          "__v": 0
        },
        "__v": 0
      }
    ]
  },
  {
    "_id": "5c7c79bcc78675135c09f554",
    "name": "Milk",
    "prices": [
      {
        "_id": "5c7c79bcc78675135c09f54e",
        "price": 1,
        "store": {
          "_id": "5c7c79bcc78675135c09f54b",
          "name": "StoreA",
          "__v": 0
        },
        "__v": 0
      },
      {
        "_id": "5c7c79bcc78675135c09f54f",
        "price": 2,
        "store": {
          "_id": "5c7c79bcc78675135c09f54c",
          "name": "StoreB",
          "__v": 0
        },
        "__v": 0
      }
    ]
  }
]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...