MarcoS
MarcoS

Reputation: 17721

MongoDB: how to insert a sub-document?

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?

Upvotes: 1

Views: 2897

Answers (2)

Lisa Gagarina
Lisa Gagarina

Reputation: 693

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.

Upvotes: 2

Zlatko
Zlatko

Reputation: 19588

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.

Upvotes: 2

Related Questions