1

I am using sub-documents in my MEAN project, to handle orders and items per order.

These are my (simplified) schemas:

var itemPerOrderSchema = new mongoose.Schema({
  itemId: String,
  count: Number
});
var OrderSchema = new mongoose.Schema({
  customerId: String,
  date: String,
  items: [ itemPerOrderSchema ]
});

To insert items in itemPerOrderSchema array I currently do:

var orderId = '123';
var item = { itemId: 'xyz', itemsCount: 7 };
Order.findOne({ id: orderId }, function(err, order) {
  order.items.push(item);
  order.save();
});

The problem is that I obviously want one item per itemId, and this way I obtain many sub-documents per item...
One solution could be to loop through all order.items, but this is not optimal, of course (order.items could me many...). The same problem could arise when querying order.items...

The question is: how do I insert items in itemPerOrderSchema array without having to loop through all items already inserted on the order?

MarcoS
  • 17,323
  • 24
  • 96
  • 174
  • itemPerOrder Schema has just items or items plus item id? I use similar format and I directly push elements into array, if you want the code then I can paste that here. – Gandalf the White Jan 10 '16 at 15:55
  • It has the itemid, too (`var itemPerOrderSchema = new mongoose.Schema({ itemId: String, ...`). If you can paste your code (in an answer, not in a comment, of course), it could help, for sure... – MarcoS Jan 10 '16 at 16:10
  • `modelname.findOneAndUpdate({_id: idname}, {$addToSet: { items: { $each: iems} }}, function(err, docs) {}` - This is what I do. – Gandalf the White Jan 10 '16 at 17:01
  • By `iems` you do mean `items`? But where is `item` to be inserted? – MarcoS Jan 10 '16 at 17:06
  • iems is item, spelling error - sorry. In my case iems is an array of string. You have to insert an object instead. – Gandalf the White Jan 10 '16 at 17:07
  • This might help you - http://www.journaldev.com/4334/mongodb-update-document-set-all-example-mongo-shell-java-driver – Gandalf the White Jan 10 '16 at 17:11
  • From MongoDb reference for $addToSet with $each: `You can use the $addToSet operator with the $each modifier. The $each modifier allows the $addToSet operator to add multiple values to the array field.`... But I don't need to add multiple values to the array field. I need to add an item to the array field if it's id is not present yet, and update the item in the array field if it is already present... – MarcoS Jan 10 '16 at 17:27

2 Answers2

2

First of all, you probably need to add orderId to your itemPerOrderSchema because the combination of orderId and itemId will make the record unique.

Assuming that orderId is added to the itemPerOrderSchema, I would suggest the following implementation:

function addItemToOrder(orderId, newItem, callback) {
  Order.findOne({ id: orderId }, function(err, order) {
    if (err) {
        return callback(err);
    }

    ItemPerOrder.findOne({ orderId: orderId, itemId: newItem.itemId }, function(err, existingItem) {
      if (err) {
        return callback(err);
      }

      if (!existingItem) {
        // there is no such item for this order yet, adding a new one
        order.items.push(newItem);
        order.save(function(err) {
          return callback(err);
        });
      }

      // there is already item with itemId for this order, updating itemsCount
      itemPerOrder.update(
        { id: existingItem.id }, 
        { $inc: { itemsCount: newItem.itemsCount }}, function(err) {
            return callback(err);
        }
      );
    });   
  });
}

addItemToOrder('123', { itemId: ‘1’, itemsCount: 7 }, function(err) {
    if (err) {
    console.log("Error", err);
  }
  console.log("Item successfully added to order");
});

Hope this may help.

Lisa Gagarina
  • 693
  • 5
  • 7
  • Thanks! It will help, probably... However, I did hope to find some way to just call order.save, after having updated order sub-document simply by pushing an element (or modifying it) to a javascript array... – MarcoS Jan 10 '16 at 18:51
  • Maybe use $inc instead of $set there for the `existingItem` update (provided count is integer) - in case you have concurrent updates. – Zlatko Jan 10 '16 at 20:12
2

If you can use an object instead of array for items, maybe you can change your schema a bit for a single-query update.

Something like this:

{
  customerId: 123,
  items: {
     xyz: 14,
     ds2: 7
  }
}

So, each itemId is a key in an object, not an element of the array.

let OrderSchema = new mongoose.Schema({
  customerId: String,
  date: String,
  items: mongoose.Schema.Types.Mixed
});

Then updating your order is super simple. Let's say you want to add 3 of items number 'xyz' to customer 123.

db.orders.update({
  customerId: 123
},
{
  $inc: {
    'items.xyz': 3
  }
}, 
{
  upsert: true
});

Passing upsert here to create the order even if the customer doesn't have an entry.

The downsides of this:

  • it is that if you use aggregation framework, it is either impossible to iterate over your items, or if you have a limited, known set of itemIds, then very verbose. You could solve that one with mapReduce, which can be a little slower, depending on how many of them you have there, so YMMB.

  • you do not have a clean items array on the client. You could fix that with either client extracting this info (a simple let items = Object.keys(order.items).map(key => ({ key: order.items[key] })); or with a mongoose virtual field or schema.path(), but this is probably another question, already answered.

Zlatko
  • 18,936
  • 14
  • 70
  • 123
  • This is exactly what I was looking for... I didn't know about Schema.Types.Mixed... Thanks! I'm only a bit worried about aggregation framework issues, since I'm currently using it for querying... Though probably I will be able to avoid iterating over items on the server, but only on the client (I'll test order.items.map() to get the items...). – MarcoS Jan 11 '16 at 11:43
  • It's `Object.keys(order.items).map();`, but that's cool. Querying and aggregation works too, and depending on what you want, it can be done. But sometimes it can be a little verbose. – Zlatko Jan 11 '16 at 20:23