node.jsmongodbmongoosemongodb-querymongoose-schema

Get DBRefs in one request. The Extended Reference Pattern


I have identical products that are sold in different cities at different prices.

Product.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const ProductSchema = new Schema({
  name: String,
  img: String,
  translations: {
    Spanish: { name: String},
  }
});
module.exports = mongoose.model("products", ProductSchema)

City.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const CitySchema = new Schema({
  name: String,
  products_cost: [{
    product_id: { type: Schema.Types.ObjectId, ref: 'products' },
    price: { type: Number, default: 0 }
  }],
  translations: {
    Spanish: { name: String },
  }
});

module.exports = mongoose.model('cities', CitySchema);

Set defaults records for products:

const Product = require('../models/Product');
const defaultValuesProducts = () => {
    try {
        Product.insertMany([
            {
                name: "Agate",
                img: "img/Agate.png",
                translations: {
                    Spanish: { name: "Ágata" },                 
                }
            },
            {
                name: "Gold",
                img: "img/Gold.png",
                translations: {
                    Spanish: { name: "Oro" },                   
                }
            },
        ]);
    } catch (error) {
        console.log(error);
    }
}
module.exports = {defaultValuesProducts};

And I'm trying to link products and cities. Set default records for cities:

const Product = require('../models/Product');
const Cities = require('../models/City');

const defaultValuesCities = () => {
    
    try {
        const  products = Product.find();
        Cities.insertMany([
            {
                name: "Amsterdam",
                products_cost: [
                    {
                        product_id: products[0]
                    },
                    {
                        product_id: products[1]
                    }                   
                ],
                shipRoute: 8,
                translations: {
                    Spanish: { name: "Ámsterdam" },                   
                }
            }
        ]);
    } catch (error) {
        console.log(error);
    }
};

module.exports = { defaultValuesCities };

In DB i see all added records:


But I can't get products from cities directly:
router.get('', async (req, res) => {
    try {
        const cities = await City.find();
        //const cities = await City.find().populate(products); - ReferenceError: products is not defined
        res.render('index', { cities });
    }
    catch (error) {
        console.log(error);
    }
});
<ol>
    <% cities.forEach(city=> { %>
        <li>
            <%=city.name%>
            <%=city.translations.Spanish.name%>
                <ol>
                    <% city.products_cost.forEach(element=> { %>
                        <li>
                            <%=element.product_id.name%>                           
                        </li>
                    <% }) %>
                </ol>
        </li>
    <% }) %>
</ol>

Please tell me where I made a mistake. And how can I access the DBRef object directly?


Solution

  • There are several problems with your code.

    1. Update Product.js with the following:
    // Change name of Model from 'products' to 'Product'
    module.exports = mongoose.model("Product", ProductSchema)
    
    1. Update City.js with the following:
    // Change name the ref from 'products' to 'Product'
    product_id: { type: Schema.Types.ObjectId, ref: 'Product' },
    

    That's because:

    The first argument is the singular name of the collection your model is for. Mongoose automatically looks for the plural, lowercased version of your model name.

    1. Contrary to what you think, your defaultValuesCities() function has not added the relevant data to your products_cost array. Yes, you have two new objects in there but you do not have the product_id, all you have the automatically generated _id and the default 0 value for price. Both your Product.find(); and Cities.insertMany() functions are asynchronous operations so your your products won't be returned in time for the values to be accessed in the Cities.insertMany() part of your code. Update like so:
    const defaultValuesCities = async () => { //< Mark function as async
        
        try {
            const products = await Product.find(); //< Mark function call with await
            await Cities.insertMany([ //< Again, mark function call with await
                {
                    name: "Amsterdam",
                    products_cost: [
                        {
                            product_id: products[0]._id //< You need to insert the _id ObjectId
                        },
                        {
                            product_id: products[1]._id //< You need to insert the _id ObjectId
                        }                   
                    ],
                    shipRoute: 8,
                    translations: {
                        Spanish: { name: "Ámsterdam" },                   
                    }
                }
            ]);
        } catch (error) {
            console.log(error);
        }
    };
    
    1. Since your product_id rerference is nested inside an array of products_cost you will need to define that path to the populate() method. Update your route like so:
    try {
       // You need to use dot notation to access 'products_cost.product_id'
       const cities = await City.find().populate({ path: 'products_cost.product_id', model: Product }); 
       res.render('index', { cities });
    } catch (error) {
       console.log(error);
    }