1

I am attempting to create an instance and multiple related instances with a many to many relation using a junction table.

While creating the multiple related instances, I need to add a value to a property on the junction table as well. I don't know if it is my lack of knowledge of sequelize or promises that is causing my problem.

The code I am using is below. This code does add the items to the database, but I need to redirect after the operation has completed, which is not working.

Basically, I need to create a Recipe. Once that is created, I need to create Ingredients and relate them to that Recipe. The ingredients are stored in an array coming from a form on an HTML page. While relating the Ingredients, I need to add the ingredient_quantity to the RecipeIngredients table, which is the through part of the relationship (the junction table).

global.db.Recipe.belongsToMany(
    global.db.Ingredient, 
    { 
        as: 'Ingredients', 
        through: global.db.RecipeIngredients, 
        foreignKey: 'recipe_id' 
    });
global.db.Ingredient.belongsToMany(
    global.db.Recipe, 
    { 
        as: 'Recipes', 
        through: global.db.RecipeIngredients, 
        foreignKey: 'ingredient_id' 
    });

router.post('/new', ensureLoggedIn, bodyParser.json(), function (req, res) {
    var recipeName = req.body.recipe_name;
    var steps = req.body.steps;
    var ingredients = req.body.ingredients;
    var ingredientQty = {};
    var currentIngredient;
    var ingredientsToAdd = [];

    db.Recipe.create({
        recipe_name: recipeName,
        directions: steps,
        FamilyId: req.user.FamilyId,
        CreatedBy: req.user._id
    })
    .then(function (recipe) {
        for (var i = 0; i < ingredients.length; i++) {

            currentIngredient = ingredients[i];
            ingredientQty[currentIngredient.ingredient_name] = 
currentIngredient.quantity;

            db.Ingredient.findOrCreate({
                where: { 
                    ingredient_name: currentIngredient.ingredient_name, 
                    FamilyId: req.user.FamilyId 
                }
            })
            .spread(function (ingredient, created) {
                if (created) {
                    console.log("Added Ingredient to DB: " + 
                    currentIngredient.ingredient_name);
                }

            ingredient.Recipes = {
                ingredient_quantity: 
                    ingredientQty[ingredient.ingredient_name]
            };
            ingredient.CreatedBy = req.user._id;
            recipe.addIngredient(ingredient)
            .then(function () {
                console.log("Added Ingredient " + ingredient.ingredient_name 
                + " to Recipe " + recipe.recipe_name);
            });
        })
    }

})
.finally(function(recipe){
    res.redirect('/recipes');
});
});

Any help would be greatly appreciated. I know that I am running into issues because of trying to use promises inside of a loop, I just don't know how else I can accomplish this.

n30r3b3l
  • 53
  • 6

2 Answers2

1

Using sequelize, you can create objects along with its associated objects in one step, provided all objects that you're creating are new. This is also called nested creation. See this link and scroll down to section titled "Creating with associations"

Coming to your issue, you've a many-to-many relationship between Recipe and Ingredient, with RecipeIngredients being the join table.

Suppose you've a new Recipe object which you want to create, like:

var myRecipe = {
  recipe_name: 'MyRecipe',
  directions: 'Easy peasy',
  FamilyId: 'someId',
  CreatedBy: 'someUserId'
}

And an array of Ingredient objects, like:

var myRecipeIngredients = [
  { ingredient_name: 'ABC', FamilyId: 'someId'},
  { ingredient_name: 'DEF', FamilyId: 'someId'},
  { ingredient_name: 'GHI', FamilyId: 'someId'}]

// associate the 2 to create in 1 step
myRecipe.Ingredients = myRecipeIngredients;

Now, you can create myRecipe and its associated myRecipeIngredients in one step as shown below:

Recipe.create(myRecipe, {include: {model: Ingredient}})
.then(function(createdObjects){
   res.json(createdObjects);
})
.catch(function(err){
   next(err);
});

And that is all !!
Sequelize will create 1 row in Recipe, 3 rows in Ingredient and 3 rows in RecipeIngredients to associate them.

Adhyatmik
  • 1,038
  • 11
  • 19
  • Ok, that helps, but how do I update the field on RecipeIngredients using this approach? When the Recipes and Ingredients are being related, I need to add the quantity to the RecipeIngredients row that gets created. The quantity is stored in the ingredient objects coming from the req.body – n30r3b3l Apr 15 '17 at 17:06
  • Oh, sorry. I totally forgot about that requirement. However, I have to admit, I havn't tried this out myself, but if you see the [documentation](http://docs.sequelizejs.com/en/latest/docs/associations/#belongs-to-many-associations) under section, "Belongs-To-Many associations", they mention that: >To add a new project to a user and set its status, you pass extra options.through to the setter, which contains the attributes for the join table. `user.addProject(project, { through: { status: 'started' }})` Hope this helps. – Adhyatmik Apr 15 '17 at 19:55
  • I have seen that documentation. What I don't understand, is how to add all of the ingredients and add set the through value for them. That example only adds a single item. It looks like I would need to add each Ingredient individually. Which brings me to the issue that I have run into. Thanks for trying though. – n30r3b3l Apr 15 '17 at 20:08
  • `models.project.create({ name: 'Sample' }) .then(function (project){ project.createTask({ name: 'Sample' }, { through: { extraColumnOnJoinTable: '123' } }) .then(res.json({ success: true })); })` Above code doesn't work. It creates a Project, then a Task and then a record in ProjectTasks (join table) but **without** the value of extraColumnOnJoinTable. I'm not sure, but you might want to open an issue with sequelize. – Adhyatmik Apr 15 '17 at 20:16
  • Do you think using Promise.Map might help out with this? I am not all that great with promuses yet, so I don't fully understand how to use the map or if it would work – n30r3b3l Apr 15 '17 at 20:47
0

I was able to fix the problem that I was having. The answers here helped me come up with my solution. I am posting the solution below in case anyone else runs into a similar issue. I created a variable to store the Promise from Recipe.create(), I used Promise.map to findOrCreate all of the ingredients from the form data. Because findOrCreate returns an array containing Promise and a boolean for if the item was created, I then had to get the actual ingredients out of the results of the Promise.map function. So I used the JavaScript array.map() function to get the first item from the arrays. And finally, use Promise.map again to add each Ingredient to the Recipe

var ingredients = req.body.ingredients,
    recipeName = req.body.recipeName,
    ingredientsQty = {}; // Used to map the ingredient and quantity for the 
                         // relationship, because of the Junction Table

var recipe = models.Recipe.create({recipe_name: recipeName});

// Use Promise.map to findOrCreate all ingredients from the form data
Promise.map(ingredients, function(ing){
    ingredientsQty[ing.ingredient_name] = ing.ingredient_quantity;
    return models.Ingredient.findOrCreate({ where: { ingredient_name: ing.ingredient_name}});
})

// Use JavaScript's Array.prototype.map function to return the ingredient 
// instance from the array returned by findOrCreate
.then(function(results){
    return results.map(function(result){
        return result[0];
    });
})

// Return the promises for the new Recipe and Ingredients
.then(function(ingredientsInDB){
    return Promise.all([recipe, ingredientsInDB]);
})

// Now I can use Promise.map again to create the relationship between the / 
// Recipe and each Ingredient
.spread(function(addedRecipe, ingredientsToAdd){
    recipe = addedRecipe;
    return Promise.map(ingredientsToAdd, function(ingredientToAdd){
        ingredientToAdd.RecipeIngredients = {
            ingredient_quantity: ingredientsQty[ingredientToAdd.ingredient_name]
        };

        return recipe.addIngredient(ingredientToAdd);
    });
})

// And here, everything is finished
.then(function(recipeWithIngredients){
   res.end
});
n30r3b3l
  • 53
  • 6