0

New to Meteor and MongoDB, coming from a relational database background. This question actually reflects my puzzle at how relations are handled in MongoDB.

Example: I want to insert a recipe into a collection of recipes, which in this case is a comma delimited string of multiple ingredients. I want, however, to insert these ingredients into a collection of ingredients at the same time. And I would like to have the recipe refer to the ingredients in the ingredients collection, so that, say, if I got an ingredient's spelling wrong the first time, I could update it in the ingredients collection later and have all recipes that use that ingredient updated as well.

It seems that the way to do it is to include the ingredients collections as subdocuments in the recipes collection.

However, I'm not sure how I can actually implement that. Sample code in JS using Meteor follows:

Recipes = new Mongo.Collection("recipes");
Ingredients = new Mongo.Collection("ingredients");

Template.body.events({
    "submit .new-recipe": function(event) {
        // Prevent default browser form submit
        event.preventDefault();

        // Get value from form element
        var text = event.target.text.value;
        var splitText = text.split(",");
        var nInputs = splitText.length;
        var recipe = [];
        for (var i = 0; i < nInputs; i++) {
            // Insert an ingredient into the ingredients collection
            Ingredients.insert({
                itemName: splitText[i].trim(),
                createdAt: new Date() 
            });
            recipe.push(splitText[i]);
        }
        // Insert the list of ingredients as a recipe into the recipes collection
        Recipes.insert({
            recipe: recipe,
            createdAt: new Date()
        });
        // Clear form
        event.target.text.value = "";
    }
});

Obviously, the above doesn't do the job correctly. It severs the relationship between the ingredients and the recipes. But how can I maintain the relationship? Do I put the Ids of the ingredients into the recipes collection at the time of inserting the ingredients? Do I have the entire ingredient document inserted as a part of a recipe document into the recipes collection, at the time of inserting the ingredients?

Community
  • 1
  • 1
MichM
  • 886
  • 1
  • 12
  • 28

1 Answers1

2

It sounds like you need a simple relational model between two collections. This is typically accomplished by storing the _id of one collection as a value in another. In your case, I'd recommend storing the ingredient ids as an array in the recipe. I see a few issues with your initial attempt:

  1. The existence of an ingredient is not being checked prior to insertion. So two recipes using "sugar" would insert two sugar documents - I assume this is not your intention.

  2. The insert is happening on the client, but unless you are publishing your whole ingredient collection, the client can't be the authority on which ingredients actually exist (following from 1).

  3. You are using the client's timestamp when doing the insert. What if their clock is wrong? There's actually a package that deals with this, but we can solve all of the above using a method.


I'd recommend splitting the text input on the client and then issuing a Meteor.call('recipes.insert', ingredientNames), where the method implementation looks something like this:

Meteor.methods({
  'recipes.insert': function(ingredientNames) {
    // Make sure the input is an array of strings.
    check(ingredientNames, [String]);

    // Use the same createdAt for all inserted documents.
    var createdAt = new Date;

    // Build an array of ingredient ids based on the input names.
    var ingredientIds = _.map(ingredientNames, function(ingredientName) {
      // Trim the input - consider toLowerCase also?
      var name = ingredientName.trim();

      // Check if an ingredient with this name already exists.
      var ingredient = Ingrediends.findOne({itemName: name});
      if (ingredient) {
        // Yes - use its id.
        return ingredient._id;
      } else {
        // Insert a new document and return its id.
        return Ingrediends.insert({
          itemName: name,
          createdAt: createdAt
        });
      }
    });

    // Insert a new recipe using the ingredient ids to join the
    // two collections.
    return Recipes.insert({
      ingredientIds: ingredientIds,
      createdAt: createdAt
    });
  }
});

Recommended reading:

David Weldon
  • 63,632
  • 11
  • 148
  • 146
  • Thanks a lot! Everything works except the `check(ingredientNames, [String]);` line. Error msg says "check" is an unknown function. What am I missing? Is it from some JS library? – MichM Nov 26 '15 at 04:44
  • Well that's weird. [check](http://docs.meteor.com/#/full/check) is a package that's included by default. Are you running a really old version of meteor? – David Weldon Nov 26 '15 at 06:15
  • Thanks for the clue! Solved by "meteor add check" – MichM Nov 26 '15 at 07:44
  • You could shrink `createdAt` as key and value are the same in the object near `Ingredients.insert` and `Recipes.insert` – Iglesias Leonardo May 09 '22 at 13:40