Querying String data from database into array Sequelize - javascript

So i basically trying to get string value from database into an Array using Sequelize:
{
"status": "success",
"data": [
{
"id": 15,
"title": "The Godfather",
"year": 1972,
"director": "Francis Ford Coppola",
"categoryFilm": "R",
"Url": "https://m.media-amazon.com/images/M/MV5BMTU4MTgxOTQ0Nl5BMl5BanBnXkFtZTgwNDI0Mjk1NDM#._V1_UY100_CR19,0,100,100_AL_.jpg,https://m.media-amazon.com/images/M/MV5BMTczMTk5MjkwOF5BMl5BanBnXkFtZTgwMDI0Mjk1NDM#._V1_UY100_CR12,0,100,100_AL_.jpg,https://m.media-amazon.com/images/M/MV5BZTFiODA5NWEtM2FhNC00MWEzLTlkYjgtMWMwNzBhYzlkY2U3XkEyXkFqcGdeQXVyMDM2NDM2MQ##._V1_UX100_CR0,0,100,100_AL_.jpg",
"genre": [
{
"genre": "Crime,Drama"
}
]
}
]
}
with my code showed as below, i try sequelize literal to change it but i cannot resolve how to turn it from string into array and split it by "," (comma):
const allFilm = async (req, res) => {
await Film.findAll({
attributes: [
"id",
"title",
"year",
"director",
[sequelize.literal(`"category"."category"`), "categoryFilm"],
[sequelize.literal(`"photo"."photoUrl"`), "Url"],
],
subQuery: false,
include: [
{
model: Genre,
as: "genre",
attributes: ["genre"],
},
{
model: Category,
as: "category",
attributes: [],
},
{
model: Photo,
as: "photo",
attributes: [],
},
],
})
.then((data) => {
res.status(200).json({
status: "success",
data: data,
});
})
.catch((err) => {
res.status(400).json({
status: err,
});
});
};
what i want is like this is there is somthing wrong with my code since i find no error but i can not resolved it into array:
"Url": ["https://m.media-amazon.com/images/M/MV5BMTU4MTgxOTQ0Nl5BMl5BanBnXkFtZTgwNDI0Mjk1NDM#._V1_UY100_CR19,0,100,100_AL_.jpg","https://m.media-amazon.com/images/M/MV5BMTczMTk5MjkwOF5BMl5BanBnXkFtZTgwMDI0Mjk1NDM#._V1_UY100_CR12,0,100,100_AL_.jpg","https://m.media-amazon.com/images/M/MV5BZTFiODA5NWEtM2FhNC00MWEzLTlkYjgtMWMwNzBhYzlkY2U3XkEyXkFqcGdeQXVyMDM2NDM2MQ##._V1_UX100_CR0,0,100,100_AL_.jpg"],

I can't answer your question, but you can maybe try that :
From Load attributes from associated model in sequelize.js
await Film.findAll({
attributes: [
"id",
"title",
"year",
"director",
[sequelize.literal(`"category"."category"`), "categoryFilm"],
[Sequelize.col('photo.photoUrl'), 'Url'] // here
],
subQuery: false,
raw:true, // here
include: [
{
model: Genre,
as: "genre",
attributes: ["genre"],
},
{
model: Category,
as: "category",
attributes: [],
},
{
model: Photo,
as: "photo",
attributes: [],
required: false, // here
},
],
})

Related

Mongoose, updated nested array

My question is:
How can I query in the nested arrays?
I want to change value in key "likeUp" which is nested inside object in array "usersWhoLiked". Where "usersWhoLiked" is nested in array "comments"
How Can I do that with mongoose ?
Request that I wrote beneath... do not work, but is very similar to answer given in StackOverflow post: Mongoose update update nested object inside an array
This is my request to db with updateOne:
try {
const response = await Comments.updateOne(
{
productId,
comments: { $elemMatch: { usersWhoLiked: { $elemMatch: { userId } } } },
},
{
$set: { 'comments.$[outer].usersWhoLiked.$[inner].likeUp': likes.up },
},
{
arrayFilters: [{ 'outer._id': commentId }, { 'inner._userId': userId }],
}
).exec();
return res.status(201).json({ response });
} catch (err) {
console.log(err);
return res.send(err);
}
This is the collection, that I am trying to update:
{
"_id": {
"$oid": "6307569d2308b78b378cc802"
},
"productId": "629da4b6634d5d11a859d729",
"comments": [
{
"userId": "62f29c2c324f4778dff443f6",
"userName": "User",
"date": "2022.08.25",
"confirmed": true,
"likes": {
"up": 0,
"down": 0
},
"content": {
"rating": 5,
"description": "Nowy komentarz"
},
"_id": {
"$oid": "630756b22308b78b378cc809"
},
"usersWhoLiked": [
{
"userId": "62f29c2c324f4778dff443f1",
"likeUp": true,
"_id": {
"$oid": "6307572d2308b78b378cc80e"
}
},
{
"userId": "62f29c2c324f4778dff443f2",
"likeUp": true,
"_id": {
"$oid": "6307572d2308b78b378cc80c"
}
}
]
}
],
"__v": 0
}
Mongooes schema for comment collection:
const commentSchema = new Schema({
productId: String,
comments: [
{
userId: String,
userName: String,
date: String,
confirmed: Boolean,
likes: {
up: {
type: Number,
default: 0,
},
down: {
type: Number,
default: 0,
},
},
content: {
rating: Number,
description: String,
},
usersWhoLiked: [{ userId: String, likeUp: Boolean }],
},
],
});
I guess the problem is with your arrayFilters operator, because you are trying to filter by field _userId which does not exist:
arrayFilters: [{ 'outer._id': commentId }, { 'inner._userId': userId }],
I managed to update the likeUp value using the following query:
db.collection.update({
_id: ObjectId("6307569d2308b78b378cc802")
},
{
$set: {
"comments.$[user].usersWhoLiked.$[like].likeUp": false
}
},
{
arrayFilters: [
{
"user._id": ObjectId("630756b22308b78b378cc809")
},
{
"like.userId": "62f29c2c324f4778dff443f1"
}
]
})
Try it on MongoDB playground: https://mongoplayground.net/p/XhQMNBgEdhp

Express - Find an object in MongoDB by id and save its data to array

I have two models: Meal and Ingredient. Here are the schemas:
const mealSchema = new Schema({
title: { type: String, required: true },
image: { type: String, required: false },
ingredients: [{
ingredient: { type: mongoose.Types.ObjectId, required: true, ref: 'Ingredient' },
amount: { type: Number, required: true }
}],
})
const ingredientSchema = new Schema({
name: { type: String, required: true },
unit: { type: String, required: true },
category: { type: String, required: false },
is_vege: { type: Boolean, required: true }
});
When I create the meal, I provide the ingredients as an array in POST request body.
const createMeal = async (req, res, next) => {
const { title, ingredients } = req.body;
// ingredientArray consists of objects with two keys:
// <String> ingredient - id of the associated ingredient in database
// <Number> amount - amount of the ingredient
ingredientArray = JSON.parse(ingredients)
const createdMeal = new Meal({
title,
image: req.file.path,
ingredients: ingredientArray
});
try {
await createdMeal.save();
} catch (err) {
const error = new HttpError('Error occurred, try again later', 500);
return next(error);
}
res.status(201).json(createdMeal);
}
This is the object that is created:
{
"_id": "62ea4531bd7e04fa740e2fee",
"title": "Spaghetti",
"image": "uploads\\images\\8da09ec6-3684-4af5-a513-f90155ddafd8.jpeg",
"ingredients": [
{
"ingredient": "62ea37251212c738a0ce9cee",
"amount": 100,
"_id": "62ea4531bd7e04fa740e2fef",
"id": "62ea4531bd7e04fa740e2fef"
},
{
"ingredient": "62ea371ab9392f3e0107c541",
"amount": 10,
"_id": "62ea4531bd7e04fa740e2ff0",
"id": "62ea4531bd7e04fa740e2ff0"
}
],
"__v": 0,
"id": "62ea4531bd7e04fa740e2fee"
}
I need my "ingredients" list to find the actual ingredient in the database and save its complete data so that the result would look something like this:
"ingredients": [
{
"ingredient": "62ea37251212c738a0ce9cee",
"amount": 100,
"name": "Pasta",
"unit": "g",
"category": "pastas",
"is_vege": true,
"_id": "62ea4531bd7e04fa740e2fef",
"id": "62ea4531bd7e04fa740e2fef"
},
{
"ingredient": "62ea371ab9392f3e0107c541",
"amount": 200,
"name": "Tomato",
"unit": "g",
"category": "vegetables",
"is_vege": true,
"_id": "62ea4531bd7e04fa740e2ff0",
"id": "62ea4531bd7e04fa740e2ff0"
}
]
EDIT: Ideally, the ingredient should be found BEFORE saving, because I want to run some calculations before the meal ends up in the database.
I figured it out. It took a single line of code:
let createdMeal = new Meal({
title,
image: req.file.path,
prep_time,
ingredients: ingredientArray
});
createdMeal = await createdMeal.populate({ path: 'ingredients.ingredient' });
Documentation:
https://mongoosejs.com/docs/populate.html

Sequelize findAndCountAll pagination issue

When using findAndCountAll with a limit and offset, I get only (for example) 8 rows per page instead of 10.
Here's what I'm using to paginate results (10 per page):
async function allPlayers(req, res) {
const page = parseInt(req.query.page);
const perPage = parseInt(req.query.perPage);
const options = {
where: {
[Op.and]: [
{
type: "player",
},
{
"$teams.team.type$": "club",
},
],
},
include: [
{
model: UserTeam,
duplicating: false,
required: true,
include: [
{
model: Team,
include: [{ model: Club }, { model: School }],
},
],
},
],
};
const { rows, count } = await User.findAndCountAll({
...options,
limit: perPage,
offset: perPage * (page - 1),
});
res.json({ data: { rows, count } });
}
The issue seems to be Sequelize filtering out the rows when returned from SQL, instead of in the query. This happens because of this segment in the find options query:
{
model: UserTeam,
duplicating: false,
required: true,
include: [...],
}
Because of that, instead of returning 10 per paginated page, it's returning 10 or less (depending if any rows were filtered out).
Is there a fix for this behaviour or a different way to re-structure my data so I don't need this nested query?
I need this because I have a database/model structure like this:
User (players, coaches, admins, etc.)
|
|_ UserTeam (pivot table containing userId and teamId)
|
|_ Team
I don't think you need to reference the pivot/join tables in the query itself. There are too many nested levels of entities in your query with School and Club etc.
If you, instead of including the nested data when you perform the query, include the User/Player model and Team model in the UserTeam model with the reference keyword, Sequelize in my experience will handle this type of operation better. The same goes for School and Club, they could be included in their join tables' model definitions instead.
Note that I use the define method and not the class based models. What if you try something like this:
const { DataTypes } = require('sequelize')
const sequelize = require('./../config/db')
const User = require('./User')
const Team = require('./Team')
const UserTeam = sequelize.define('userTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
teamId: {
type: DataTypes.UUID,
allowNull: false,
unique: false,
onDelete: 'CASCADE',
references: {
model: Team,
key: 'id'
},
validate: {
isUUID: {
args: 4,
msg: 'Team ID must be a UUID4 string.'
}
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
unique: false,
onDelete: 'CASCADE',
references: {
model: User,
key: 'id'
},
validate: {
isUUID: {
args: 4,
msg: 'User ID must be a UUID4 string.'
}
}
},
deletedAt: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null
}
},
{
paranoid: true,
tableName: 'userTeam'
})
Then you include the Team like this:
const users = await User.findAll({
include: Team
})
Associations (it would probably be more logical to call the join table TeamUser and not UserTeam):
User.belongsToMany(Team, { through: UserTeam, foreignKey: 'userId', onDelete: 'CASCADE' })
Team.belongsToMany(User, { through: UserTeam, foreignKey: 'teamId', onDelete: 'CASCADE' })
A json response based on this many-to-many relationship and the query above should result in a list of all users with every team that user is connected to. Here is one of those users:
{
"fullName": "Player One",
"id": "6e8ca258-9daa-4d52-b033-a077d98c29ef",
"firstName": "Player",
"lastName": "One",
"email": "playerone#gmail.com",
"password": "$2a$14$tApwpX9Ld9a1cjZMFzTGZeVEUC01M7n/tSVlldG7OEbm9sEh/k8kW",
"verified": 1,
"verifyCode": "4f49b5ca-12ed5a2d-1608191096291",
"resetPasswordToken": "bd15eda5097e030988eed5d2d20b3bbb6a06439f6b9907783fc0ea19083d8410",
"resetPasswordExpire": 1620266644543,
"passwordChangedAt": 1608337842942,
"createdFromIp": "122.222.10.33",
"createdAt": "2020-12-14T06:33:13.000Z",
"updatedAt": "2021-05-06T01:54:04.000Z",
"deletedAt": null,
"teams": [
{
"id": "427e9de4-9318-4406-aed9-fcbb3b8a3282",
"name": "Texas Rangers",
"type": "MLB",
"slug": "texas-rangers",
"createdByUserId": "6e8ca258-9daa-4d52-b033-a077d98c29ef",
"createdAt": "2020-12-14T06:33:15.000Z",
"updatedAt": "2020-12-29T06:07:54.000Z",
"deletedAt": null,
"userTeams": {
"id": 54,
"teamId": "427e9de4-9318-4406-aed9-fcbb3b8a3282",
"userId": "6e8ca258-9daa-4d52-b033-a077d98c29ef",
"deletedAt": null,
"createdAt": "2020-12-14T06:33:15.000Z",
"updatedAt": "2020-12-14T06:33:15.000Z"
}
},
{
"id": "cbff6df7-0e0c-4906-9e1c-54b569079d83",
"name": "New York Yankees",
"type": "MLB",
"slug": "yankees",
"createdByUserId": "16f38fc5-63d1-4285-8773-526720f9a506",
"createdAt": "2020-12-14T23:57:11.000Z",
"updatedAt": "2021-07-06T06:08:35.000Z",
"deletedAt": null,
"userTeams": {
"id": 55,
"teamId": "cbff6df7-0e0c-4906-9e1c-54b569079d83",
"userId": "6e8ca258-9daa-4d52-b033-a077d98c29ef",
"deletedAt": null,
"createdAt": "2020-12-14T23:57:11.000Z",
"updatedAt": "2020-12-14T23:57:11.000Z"
}
}
]
}
Let try this function bro
paginate: ({
currentPage,
pageSize
}) => {
const offset = parseInt((currentPage - 1) * pageSize, 10);
const limit = parseInt(pageSize, 10);
return {
offset,
limit,
};
},
// import function here
const result = await city.findAndCountAll({
where: conditions,
order: [
['createdAt', 'DESC']
],
...paginate({
currentPage: page,
pageSize: limit
}),
})

Add a new field based on others to a document every time it's fetched

My Question schema looks like this:
const questionSchema = new mongoose.Schema({
content: String,
options: [{
content: String,
correct: Boolean
}]
});
I also have a Test schema, where I refer to Question:
const testSchema = new mongoose.Schema({
// ...
questions: [{
type: mongoose.Schema.Types.ObjectId,
ref: "Question"
}]
})
When I fetch Questions (using find(), findOne() or Test.find().populate("questions")) I'd like to add to the document a new boolean field multiple based to on how many options have correct === true. Expected output:
{
_id: "...",
_v: 1,
content: "What is a capital of Slovenia?"
options: [
{
content: "Bled",
correct: false
},
{
content: "Ljubljana",
correct: true
}
],
multiple: false
}
Is it possible to use some kind of a function which is called everytime I query Question and adds a new field to a fetched object or do I have to store multiple field permanently in Mongo?
Depending on your needs there are a couple of approaches here.
Mongoose Virtual Field
The most direct should be since you are using mongoose would be to add a virtual field to the schema which basically calculates it's value when it is accessed. You don't specify your logic in the question, but presuming something like "more than one true" means that multiple is true then you would do something like this:
const questionSchema = new Schema({
content: String,
options: [{
content: String,
correct: Boolean
}]
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
questionSchema.virtual('multiple').get(function() {
return this.options.filter(e => e.correct).length > 1;
});
That's a basic "getter" which simply looks at the array content and returns if the number of true elements for the correct property are more than one within the array content. It can really be whatever logic you want in the function. Note the use of function() and not () => since "arrow functions" have a different scope of this and that is important to mongoose to determine the current instance value at the time of evaluation.
The toJSON and toObject options in the schema definition are optional, but basically their point is that you can access the "virtual" property directly ( i.e question.multiple === false ) but something like console.log(question) does NOT show the virtual properties unless that definition is added with those options.
MongoDB Projection
Another option is to just have MongoDB do the work to return the modified document from the server in results. This is done using the aggregation framework, which is basically the tool for any "result manipulation".
Here as an example we implement the same logic as presented in the virtual method, along with using $lookup in the same way a populate() would be done. Except of course this is one request to the server and not two as would be the case with populate(), which simply issues a separate query for the "related" data:
// Logic in aggregate result
let result = await Test.aggregate([
{ "$lookup": {
"from": Question.collection.name,
"let": { "questions": "$questions" },
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$questions" ]
}
}},
{ "$addFields": {
"multiple": {
"$gt": [
{ "$size": {
"$filter": {
"input": "$options",
"cond": "$$this.correct"
}
}},
1
]
}
}}
],
"as": "questions"
}}
]);
Same sort of operations with $filter instead of Array.filter() and $size instead of Array.length. Again the main benefit is the "server join" here, so it's possibly better for you to implement the "virtual" logic there rather than on the schema.
Whilst it is "possible" to do things like using an aggregate() result with mongoose schema and methods, the default behavior is that aggregate() returns "plain objects"
and not the "mongoose document" instances which have the schema methods. You could re-cast the results and use schema methods, but that's probably going to mean defining "special" schema and model classes just for specific "aggregation" results, and probably not the most efficient thing to do.
Overall which one you implement depends on which suits your application needs the best.
And of course whilst you "could" also just store the same data in the MongoDB document instead of calculating each time it's retrieved, then the overhead basically shifts to the time of writing the data, where mostly this would depend on how you write data. For instance if you "add new options" to existing options then you basically need to read the whole document from MongoDB, inspect the content and then decide what to write back for the multiple value. So the same logic presented here ( more than one true in the array ) has no "atomic" write process that can be done without reading the document data first.
As a working example of these approaches, see the following listing:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test';
const opts = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);
const questionSchema = new Schema({
content: String,
options: [{
content: String,
correct: Boolean
}]
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
questionSchema.virtual('multiple').get(function() {
return this.options.filter(e => e.correct).length > 1;
});
const testSchema = new Schema({
questions: [{
type: Schema.Types.ObjectId,
ref: 'Question'
}]
});
const Question = mongoose.model('Question', questionSchema);
const Test = mongoose.model('Test', testSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Insert some data
let questions = await Question.insertMany([
{
"content": "What is the a capital of Slovenia?",
"options": [
{ "content": "Bled", "correct": false },
{ "content": "Ljubljana", "correct": true }
]
},
{
"content": "Who are the most excellent people?",
"options": [
{ "content": "Bill", "correct": true },
{ "content": "Ted", "correct": true },
{ "content": "Evil Duke", "correct": false }
]
}
]);
await Test.create({ questions })
// Just the questions
let qresults = await Question.find();
log(qresults);
// Test with populated questions
let test = await Test.findOne().populate('questions');
log(test);
// Logic in aggregate result
let result = await Test.aggregate([
{ "$lookup": {
"from": Question.collection.name,
"let": { "questions": "$questions" },
"pipeline": [
{ "$match": {
"$expr": {
"$in": [ "$_id", "$$questions" ]
}
}},
{ "$addFields": {
"multiple": {
"$gt": [
{ "$size": {
"$filter": {
"input": "$options",
"cond": "$$this.correct"
}
}},
1
]
}
}}
],
"as": "questions"
}}
]);
log(result);
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
And it's output:
Mongoose: questions.deleteMany({}, {})
Mongoose: tests.deleteMany({}, {})
Mongoose: questions.insertMany([ { _id: 5cce2f0b83d75c2d1fe6f728, content: 'What is the a capital of Slovenia?', options: [ { _id: 5cce2f0b83d75c2d1fe6f72a, content: 'Bled', correct: false }, { _id: 5cce2f0b83d75c2d1fe6f729, content: 'Ljubljana', correct: true } ], __v: 0 }, { _id: 5cce2f0b83d75c2d1fe6f72b, content: 'Who are the most excellent people?', options: [ { _id: 5cce2f0b83d75c2d1fe6f72e, content: 'Bill', correct: true }, { _id: 5cce2f0b83d75c2d1fe6f72d, content: 'Ted', correct: true }, { _id: 5cce2f0b83d75c2d1fe6f72c, content: 'Evil Duke', correct: false } ], __v: 0 } ], {})
Mongoose: tests.insertOne({ questions: [ ObjectId("5cce2f0b83d75c2d1fe6f728"), ObjectId("5cce2f0b83d75c2d1fe6f72b") ], _id: ObjectId("5cce2f0b83d75c2d1fe6f72f"), __v: 0 })
Mongoose: questions.find({}, { projection: {} })
[
{
"_id": "5cce2f0b83d75c2d1fe6f728",
"content": "What is the a capital of Slovenia?",
"options": [
{
"_id": "5cce2f0b83d75c2d1fe6f72a",
"content": "Bled",
"correct": false
},
{
"_id": "5cce2f0b83d75c2d1fe6f729",
"content": "Ljubljana",
"correct": true
}
],
"__v": 0,
"multiple": false,
"id": "5cce2f0b83d75c2d1fe6f728"
},
{
"_id": "5cce2f0b83d75c2d1fe6f72b",
"content": "Who are the most excellent people?",
"options": [
{
"_id": "5cce2f0b83d75c2d1fe6f72e",
"content": "Bill",
"correct": true
},
{
"_id": "5cce2f0b83d75c2d1fe6f72d",
"content": "Ted",
"correct": true
},
{
"_id": "5cce2f0b83d75c2d1fe6f72c",
"content": "Evil Duke",
"correct": false
}
],
"__v": 0,
"multiple": true,
"id": "5cce2f0b83d75c2d1fe6f72b"
}
]
Mongoose: tests.findOne({}, { projection: {} })
Mongoose: questions.find({ _id: { '$in': [ ObjectId("5cce2f0b83d75c2d1fe6f728"), ObjectId("5cce2f0b83d75c2d1fe6f72b") ] } }, { projection: {} })
{
"questions": [
{
"_id": "5cce2f0b83d75c2d1fe6f728",
"content": "What is the a capital of Slovenia?",
"options": [
{
"_id": "5cce2f0b83d75c2d1fe6f72a",
"content": "Bled",
"correct": false
},
{
"_id": "5cce2f0b83d75c2d1fe6f729",
"content": "Ljubljana",
"correct": true
}
],
"__v": 0,
"multiple": false,
"id": "5cce2f0b83d75c2d1fe6f728"
},
{
"_id": "5cce2f0b83d75c2d1fe6f72b",
"content": "Who are the most excellent people?",
"options": [
{
"_id": "5cce2f0b83d75c2d1fe6f72e",
"content": "Bill",
"correct": true
},
{
"_id": "5cce2f0b83d75c2d1fe6f72d",
"content": "Ted",
"correct": true
},
{
"_id": "5cce2f0b83d75c2d1fe6f72c",
"content": "Evil Duke",
"correct": false
}
],
"__v": 0,
"multiple": true,
"id": "5cce2f0b83d75c2d1fe6f72b"
}
],
"_id": "5cce2f0b83d75c2d1fe6f72f",
"__v": 0
}
Mongoose: tests.aggregate([ { '$lookup': { from: 'questions', let: { questions: '$questions' }, pipeline: [ { '$match': { '$expr': { '$in': [ '$_id', '$$questions' ] } } }, { '$addFields': { multiple: { '$gt': [ { '$size': { '$filter': { input: '$options', cond: '$$this.correct' } } }, 1 ] } } } ], as: 'questions' } } ], {})
[
{
"_id": "5cce2f0b83d75c2d1fe6f72f",
"questions": [
{
"_id": "5cce2f0b83d75c2d1fe6f728",
"content": "What is the a capital of Slovenia?",
"options": [
{
"_id": "5cce2f0b83d75c2d1fe6f72a",
"content": "Bled",
"correct": false
},
{
"_id": "5cce2f0b83d75c2d1fe6f729",
"content": "Ljubljana",
"correct": true
}
],
"__v": 0,
"multiple": false
},
{
"_id": "5cce2f0b83d75c2d1fe6f72b",
"content": "Who are the most excellent people?",
"options": [
{
"_id": "5cce2f0b83d75c2d1fe6f72e",
"content": "Bill",
"correct": true
},
{
"_id": "5cce2f0b83d75c2d1fe6f72d",
"content": "Ted",
"correct": true
},
{
"_id": "5cce2f0b83d75c2d1fe6f72c",
"content": "Evil Duke",
"correct": false
}
],
"__v": 0,
"multiple": true
}
],
"__v": 0
}
]

Many to Many with Mongoose

I have the two models:
Item.js
const mongoose = require('mongoose');
const itemSchema = new mongoose.Schema({
name: String,
stores: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Store' }]
});
const Item = mongoose.model('Item', itemSchema);
module.exports = Item;
Store.js
const mongoose = require('mongoose');
const storeSchema = new mongoose.Schema({
name: String,
items: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Item' }]
});
const Store = mongoose.model('Store', storeSchema);
module.exports = Store;
And a seed.js file:
const faker = require('faker');
const Store = require('./models/Store');
const Item = require('./models/Item');
console.log('Seeding..');
let item = new Item({
name: faker.name.findName() + " Item"
});
item.save((err) => {
if (err) return;
let store = new Store({
name: faker.name.findName() + " Store"
});
store.items.push(item);
store.save((err) => {
if (err) return;
})
});
The store is saved with the items array containing 1 item. The item though, doesn't have stores. What am I missing? How to automatically update the many-to-many relationships in MongoDB/Mongoose? I was used to Rails and everything was done automatically.
The problem you presently have is that you saved the reference in one model but you did not save it in the other. There is no "automatic referential integrity" in MongoDB, and such concept of "relations" are really a "manual" affair, and in fact the case with .populate() is actually a whole bunch of additional queries in order to retrieve the referenced information. No "magic" here.
Correct handling of "many to many" comes down to three options:
Listing 1 - Keep arrays on Both documents
Following your current design, the parts you are missing is storing the referenced on "both" the related items. For a listing to demonstrate:
const { Schema } = mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
const itemSchema = new Schema({
name: String,
stores: [{ type: Schema.Types.ObjectId, ref: 'Store' }]
});
const storeSchema = new Schema({
name: String,
items: [{ type: Schema.Types.ObjectId, ref: 'Item' }]
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const log = data => console.log(JSON.stringify(data,undefined,2))
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany() )
);
// Create some instances
let [toothpaste,brush] = ['toothpaste','brush'].map(
name => new Item({ name })
);
let [billsStore,tedsStore] = ['Bills','Teds'].map(
name => new Store({ name })
);
// Add items to stores
[billsStore,tedsStore].forEach( store => {
store.items.push(toothpaste); // add toothpaste to store
toothpaste.stores.push(store); // add store to toothpaste
});
// Brush is only in billsStore
billsStore.items.push(brush);
brush.stores.push(billsStore);
// Save everything
await Promise.all(
[toothpaste,brush,billsStore,tedsStore].map( m => m.save() )
);
// Show stores
let stores = await Store.find().populate('items','-stores');
log(stores);
// Show items
let items = await Item.find().populate('stores','-items');
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
This creates the "items" collection:
{
"_id" : ObjectId("59ab96d9c079220dd8eec428"),
"name" : "toothpaste",
"stores" : [
ObjectId("59ab96d9c079220dd8eec42a"),
ObjectId("59ab96d9c079220dd8eec42b")
],
"__v" : 0
}
{
"_id" : ObjectId("59ab96d9c079220dd8eec429"),
"name" : "brush",
"stores" : [
ObjectId("59ab96d9c079220dd8eec42a")
],
"__v" : 0
}
And the "stores" collection:
{
"_id" : ObjectId("59ab96d9c079220dd8eec42a"),
"name" : "Bills",
"items" : [
ObjectId("59ab96d9c079220dd8eec428"),
ObjectId("59ab96d9c079220dd8eec429")
],
"__v" : 0
}
{
"_id" : ObjectId("59ab96d9c079220dd8eec42b"),
"name" : "Teds",
"items" : [
ObjectId("59ab96d9c079220dd8eec428")
],
"__v" : 0
}
And produces overall output such as:
Mongoose: items.deleteMany({}, {})
Mongoose: stores.deleteMany({}, {})
Mongoose: items.insertOne({ name: 'toothpaste', _id: ObjectId("59ab96d9c079220dd8eec428"), stores: [ ObjectId("59ab96d9c079220dd8eec42a"), ObjectId("59ab96d9c079220dd8eec42b") ], __v: 0 })
Mongoose: items.insertOne({ name: 'brush', _id: ObjectId("59ab96d9c079220dd8eec429"), stores: [ ObjectId("59ab96d9c079220dd8eec42a") ], __v: 0 })
Mongoose: stores.insertOne({ name: 'Bills', _id: ObjectId("59ab96d9c079220dd8eec42a"), items: [ ObjectId("59ab96d9c079220dd8eec428"), ObjectId("59ab96d9c079220dd8eec429") ], __v: 0 })
Mongoose: stores.insertOne({ name: 'Teds', _id: ObjectId("59ab96d9c079220dd8eec42b"), items: [ ObjectId("59ab96d9c079220dd8eec428") ], __v: 0 })
Mongoose: stores.find({}, { fields: {} })
Mongoose: items.find({ _id: { '$in': [ ObjectId("59ab96d9c079220dd8eec428"), ObjectId("59ab96d9c079220dd8eec429") ] } }, { fields: { stores: 0 } })
[
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0,
"items": [
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0
},
{
"_id": "59ab96d9c079220dd8eec429",
"name": "brush",
"__v": 0
}
]
},
{
"_id": "59ab96d9c079220dd8eec42b",
"name": "Teds",
"__v": 0,
"items": [
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0
}
]
}
]
Mongoose: items.find({}, { fields: {} })
Mongoose: stores.find({ _id: { '$in': [ ObjectId("59ab96d9c079220dd8eec42a"), ObjectId("59ab96d9c079220dd8eec42b") ] } }, { fields: { items: 0 } })
[
{
"_id": "59ab96d9c079220dd8eec428",
"name": "toothpaste",
"__v": 0,
"stores": [
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0
},
{
"_id": "59ab96d9c079220dd8eec42b",
"name": "Teds",
"__v": 0
}
]
},
{
"_id": "59ab96d9c079220dd8eec429",
"name": "brush",
"__v": 0,
"stores": [
{
"_id": "59ab96d9c079220dd8eec42a",
"name": "Bills",
"__v": 0
}
]
}
]
The key points being that you actually add the reference data to each document in each collection where a relationship exists. The "arrays" present are used here to store those references and "lookup" the results from the related collection and replace them with the object data that was stored there.
Pay attention to parts like:
// Add items to stores
[billsStore,tedsStore].forEach( store => {
store.items.push(toothpaste); // add toothpaste to store
toothpaste.stores.push(store); // add store to toothpaste
});
Because that means not only are we adding the toothpaste to the "items" array in each store, but we are also adding each "store" to the "stores" array of the toothpaste item. This is done so the relationships can work being queried from either direction. If you only wanted "items from stores" and never "stores from items", then you would not need to store the relation data on the "item" entries at all.
Listing 2 - Use Virtuals and an Intermediary Collection
This is essentially the classic "many to many" relation. Where instead of directly defining relationships between the two collections, there is another collection ( table ) that stores the details about which item is related to which store.
As a full listing:
const { Schema } = mongoose = require('mongoose');
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
const itemSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
const storeSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
storeSchema.virtual('items', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'storeId'
});
const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const StoreItem = mongoose.model('StoreItem', storeItemSchema);
const log = data => console.log(JSON.stringify(data,undefined,2));
(async function() {
try {
const conn = await mongoose.connect(uri,options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany() )
);
// Create some instances
let [toothpaste,brush] = await Item.insertMany(
['toothpaste','brush'].map( name => ({ name }) )
);
let [billsStore,tedsStore] = await Store.insertMany(
['Bills','Teds'].map( name => ({ name }) )
);
// Add toothpaste to both stores
for( let store of [billsStore,tedsStore] ) {
await StoreItem.update(
{ storeId: store._id, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
// Add brush to billsStore
await StoreItem.update(
{ storeId: billsStore._id, itemId: brush._id },
{},
{ 'upsert': true }
);
// Show stores
let stores = await Store.find().populate({
path: 'items',
populate: { path: 'itemId' }
});
log(stores);
// Show Items
let items = await Item.find().populate({
path: 'stores',
populate: { path: 'storeId' }
});
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})();
The relations are now in their own collection, so the data now appears differently, for "items":
{
"_id" : ObjectId("59ab996166d5cc0e0d164d74"),
"__v" : 0,
"name" : "toothpaste"
}
{
"_id" : ObjectId("59ab996166d5cc0e0d164d75"),
"__v" : 0,
"name" : "brush"
}
And "stores":
{
"_id" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0,
"name" : "Bills"
}
{
"_id" : ObjectId("59ab996166d5cc0e0d164d77"),
"__v" : 0,
"name" : "Teds"
}
And now for "storeitems" which maps the relations:
{
"_id" : ObjectId("59ab996179e41cc54405b72b"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d74"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0
}
{
"_id" : ObjectId("59ab996179e41cc54405b72d"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d74"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d77"),
"__v" : 0
}
{
"_id" : ObjectId("59ab996179e41cc54405b72f"),
"itemId" : ObjectId("59ab996166d5cc0e0d164d75"),
"storeId" : ObjectId("59ab996166d5cc0e0d164d76"),
"__v" : 0
}
With full output like:
Mongoose: items.deleteMany({}, {})
Mongoose: stores.deleteMany({}, {})
Mongoose: storeitems.deleteMany({}, {})
Mongoose: items.insertMany([ { __v: 0, name: 'toothpaste', _id: 59ab996166d5cc0e0d164d74 }, { __v: 0, name: 'brush', _id: 59ab996166d5cc0e0d164d75 } ])
Mongoose: stores.insertMany([ { __v: 0, name: 'Bills', _id: 59ab996166d5cc0e0d164d76 }, { __v: 0, name: 'Teds', _id: 59ab996166d5cc0e0d164d77 } ])
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d74"), storeId: ObjectId("59ab996166d5cc0e0d164d76") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d74"), storeId: ObjectId("59ab996166d5cc0e0d164d77") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: storeitems.update({ itemId: ObjectId("59ab996166d5cc0e0d164d75"), storeId: ObjectId("59ab996166d5cc0e0d164d76") }, { '$setOnInsert': { __v: 0 } }, { upsert: true })
Mongoose: stores.find({}, { fields: {} })
Mongoose: storeitems.find({ storeId: { '$in': [ ObjectId("59ab996166d5cc0e0d164d76"), ObjectId("59ab996166d5cc0e0d164d77") ] } }, { fields: {} })
Mongoose: items.find({ _id: { '$in': [ ObjectId("59ab996166d5cc0e0d164d74"), ObjectId("59ab996166d5cc0e0d164d75") ] } }, { fields: {} })
[
{
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": [
{
"_id": "59ab996179e41cc54405b72b",
"itemId": {
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": null,
"id": "59ab996166d5cc0e0d164d74"
},
"storeId": "59ab996166d5cc0e0d164d76",
"__v": 0
},
{
"_id": "59ab996179e41cc54405b72f",
"itemId": {
"_id": "59ab996166d5cc0e0d164d75",
"__v": 0,
"name": "brush",
"stores": null,
"id": "59ab996166d5cc0e0d164d75"
},
"storeId": "59ab996166d5cc0e0d164d76",
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d76"
},
{
"_id": "59ab996166d5cc0e0d164d77",
"__v": 0,
"name": "Teds",
"items": [
{
"_id": "59ab996179e41cc54405b72d",
"itemId": {
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": null,
"id": "59ab996166d5cc0e0d164d74"
},
"storeId": "59ab996166d5cc0e0d164d77",
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d77"
}
]
Mongoose: items.find({}, { fields: {} })
Mongoose: storeitems.find({ itemId: { '$in': [ ObjectId("59ab996166d5cc0e0d164d74"), ObjectId("59ab996166d5cc0e0d164d75") ] } }, { fields: {} })
Mongoose: stores.find({ _id: { '$in': [ ObjectId("59ab996166d5cc0e0d164d76"), ObjectId("59ab996166d5cc0e0d164d77") ] } }, { fields: {} })
[
{
"_id": "59ab996166d5cc0e0d164d74",
"__v": 0,
"name": "toothpaste",
"stores": [
{
"_id": "59ab996179e41cc54405b72b",
"itemId": "59ab996166d5cc0e0d164d74",
"storeId": {
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": null,
"id": "59ab996166d5cc0e0d164d76"
},
"__v": 0
},
{
"_id": "59ab996179e41cc54405b72d",
"itemId": "59ab996166d5cc0e0d164d74",
"storeId": {
"_id": "59ab996166d5cc0e0d164d77",
"__v": 0,
"name": "Teds",
"items": null,
"id": "59ab996166d5cc0e0d164d77"
},
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d74"
},
{
"_id": "59ab996166d5cc0e0d164d75",
"__v": 0,
"name": "brush",
"stores": [
{
"_id": "59ab996179e41cc54405b72f",
"itemId": "59ab996166d5cc0e0d164d75",
"storeId": {
"_id": "59ab996166d5cc0e0d164d76",
"__v": 0,
"name": "Bills",
"items": null,
"id": "59ab996166d5cc0e0d164d76"
},
"__v": 0
}
],
"id": "59ab996166d5cc0e0d164d75"
}
]
Since the relations are now mapped in a separate collection there are a couple of changes here. Notably we want to define a "virtual" field on the collection that no longer has a fixed array of items. So you add one as is shown:
const itemSchema = new Schema({
name: String,
},{
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
You assign the virtual field with it's localField and foreignField mappings so the subsequent .populate() call knows what to use.
The intermediary collection has a fairly standard definition:
const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
And instead of "pushing" new items onto arrays, we instead add them to this new collection. A reasonable method for this is using "upserts" to create a new entry only when this combination does not exist:
// Add toothpaste to both stores
for( let store of [billsStore,tedsStore] ) {
await StoreItem.update(
{ storeId: store._id, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
It's a pretty simple method that merely creates a new document with the two keys supplied in the query where one was not found, or essentially tries to update the same document when matched, and with "nothing" in this case. So existing matches just end up as a "no-op", which is the desired thing to do. Alternately you could simply .insertOne() an ignore duplicate key errors. Whatever takes your fancy.
Actually querying this "related" data works a little differently again. Because there is another collection involved, we call .populate() in a way that considers it needs to "lookup" the relation on other retrieved property as well. So you have calls like this:
// Show stores
let stores = await Store.find().populate({
path: 'items',
populate: { path: 'itemId' }
});
log(stores);
Listing 3 - Use Modern Features to do it on the server
So depending on which approach taken, being using arrays or an intermediary collection to store the relation data in as an alternative to "growing arrays" within the documents, then the obvious thing you should be noting is that the .populate() calls used are actually making additional queries to MongoDB and pulling those documents over the network in separate requests.
This might appear all well and fine in small doses, however as things scale up and especially over volumes of requests, this is never a good thing. Additionally there might well be other conditions you want to apply that means you don't need to pull all the documents from the server, and would rather match data from those "relations" before you returned results.
This is why modern MongoDB releases include $lookup which actually "joins" the data on the server itself. By now you should have been looking at all the output those API calls produce as shown by mongoose.set('debug',true).
So instead of producing multiple queries, this time we make it one aggregation statement to "join" on the server, and return the results in one request:
// Show Stores
let stores = await Store.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$storeId' ] }
}},
{ '$lookup': {
'from': Item.collection.name,
'let': { 'itemId': '$itemId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$itemId' ] }
}}
],
'as': 'items'
}},
{ '$unwind': '$items' },
{ '$replaceRoot': { 'newRoot': '$items' } }
],
'as': 'items'
}}
])
log(stores);
Which whilst longer in coding, is actually far superior in efficiency even for the very trivial action right here. This of course scales considerably.
Following the same "intermediary" model as before ( and just for example, because it could be done either way ) we have a full listing:
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/manydemo',
options = { useNewUrlParser: true };
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
const itemSchema = new Schema({
name: String
}, {
toJSON: { virtuals: true }
});
itemSchema.virtual('stores', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'itemId'
});
const storeSchema = new Schema({
name: String
}, {
toJSON: { virtuals: true }
});
storeSchema.virtual('items', {
ref: 'StoreItem',
localField: '_id',
foreignField: 'storeId'
});
const storeItemSchema = new Schema({
storeId: { type: Schema.Types.ObjectId, ref: 'Store', required: true },
itemId: { type: Schema.Types.ObjectId, ref: 'Item', required: true }
});
const Item = mongoose.model('Item', itemSchema);
const Store = mongoose.model('Store', storeSchema);
const StoreItem = mongoose.model('StoreItem', storeItemSchema);
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Create some instances
let [toothpaste, brush] = await Item.insertMany(
['toothpaste', 'brush'].map(name => ({ name }) )
);
let [billsStore, tedsStore] = await Store.insertMany(
['Bills', 'Teds'].map( name => ({ name }) )
);
// Add toothpaste to both stores
for ( let { _id: storeId } of [billsStore, tedsStore] ) {
await StoreItem.updateOne(
{ storeId, itemId: toothpaste._id },
{ },
{ 'upsert': true }
);
}
// Add brush to billsStore
await StoreItem.updateOne(
{ storeId: billsStore._id, itemId: brush._id },
{ },
{ 'upsert': true }
);
// Show Stores
let stores = await Store.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$storeId' ] }
}},
{ '$lookup': {
'from': Item.collection.name,
'let': { 'itemId': '$itemId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$itemId' ] }
}}
],
'as': 'items'
}},
{ '$unwind': '$items' },
{ '$replaceRoot': { 'newRoot': '$items' } }
],
'as': 'items'
}}
])
log(stores);
// Show Items
let items = await Item.aggregate([
{ '$lookup': {
'from': StoreItem.collection.name,
'let': { 'id': '$_id' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$$id', '$itemId' ] }
}},
{ '$lookup': {
'from': Store.collection.name,
'let': { 'storeId': '$storeId' },
'pipeline': [
{ '$match': {
'$expr': { '$eq': [ '$_id', '$$storeId' ] }
}}
],
'as': 'stores',
}},
{ '$unwind': '$stores' },
{ '$replaceRoot': { 'newRoot': '$stores' } }
],
'as': 'stores'
}}
]);
log(items);
} catch(e) {
console.error(e);
} finally {
mongoose.disconnect();
}
})()
And the output:
Mongoose: stores.aggregate([ { '$lookup': { from: 'storeitems', let: { id: '$_id' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$$id', '$storeId' ] } } }, { '$lookup': { from: 'items', let: { itemId: '$itemId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$itemId' ] } } } ], as: 'items' } }, { '$unwind': '$items' }, { '$replaceRoot': { newRoot: '$items' } } ], as: 'items' } } ], {})
[
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0,
"items": [
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0
},
{
"_id": "5ca7210717dadc69652b37d9",
"name": "brush",
"__v": 0
}
]
},
{
"_id": "5ca7210717dadc69652b37db",
"name": "Teds",
"__v": 0,
"items": [
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0
}
]
}
]
Mongoose: items.aggregate([ { '$lookup': { from: 'storeitems', let: { id: '$_id' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$$id', '$itemId' ] } } }, { '$lookup': { from: 'stores', let: { storeId: '$storeId' }, pipeline: [ { '$match': { '$expr': { '$eq': [ '$_id', '$$storeId' ] } } } ], as: 'stores' } }, { '$unwind': '$stores' }, { '$replaceRoot': { newRoot: '$stores' } } ], as: 'stores' } } ], {})
[
{
"_id": "5ca7210717dadc69652b37d8",
"name": "toothpaste",
"__v": 0,
"stores": [
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0
},
{
"_id": "5ca7210717dadc69652b37db",
"name": "Teds",
"__v": 0
}
]
},
{
"_id": "5ca7210717dadc69652b37d9",
"name": "brush",
"__v": 0,
"stores": [
{
"_id": "5ca7210717dadc69652b37da",
"name": "Bills",
"__v": 0
}
]
}
]
What should be obvious is the significant reduction in the queries issued on the end to return the "joined" form of the data. This means lower latency and more responsive applications as a result of removing all the network overhead.
Final Notes
Those a are generally your approaches to dealing with "many to many" relations, which essentially comes down to either:
Keeping arrays in each document on either side holding the references to the related items.
Storing an intermediary collection and using that as a lookup reference to retrieving the other data.
In all cases it is up to you to actually store those references if you expect things to work on "both directions". Of course $lookup and even "virtuals" where that applies means that you don't always need to store on every source since you could then "reference" in just one place and use that information by applying those methods.
The other case is of course "embedding", which is an entirely different game and what document oriented databases such as MongoDB are really all about. Therefore instead of "fetching from another collection" the concept is of course to "embed" the data.
This means not just the ObjectId values that point to the other items, but actually storing the full data within arrays in each document. There is of course an issue of "size" and of course issues with updating data in multiple places. This is generally the trade off for there being a single request and a simple request that does not need to go and find data in other collections because it's "already there".
There is plenty of material around on the subject of referencing vs embedding. Once such summary source is Mongoose populate vs object nesting or even the very general MongoDB relationships: embed or reference? and many many others.
You should spend some time thinking about the concepts and how this applies to your application in general. And note that you are not actually using an RDBMS here, so you might as well use the correct features that you are meant to exploit, rather than simply making one act like the other.
You first should consider the usage of data in your application before modeling the database.
I don't have the detailed requirements of your application. But why do you have to keep 2 references in 2 schemas? Why not just keep 1 reference from Store to Item (which means 1 store has many items), and then if you want execute a query to find which stores does a item belong to, there is also away to do it by querying Store collection.
In addition, there is nothing called "many-to-many" in MongoDB. It depends on how the data is being used that you must figure out the efficient way to form the relationship between collections, as well as to structure your database.
Anyway, if you still want to use your current schemas, you can first create the item, then create the store and push the created item's id in to items array, then execute a update to the item with created store's id.

Categories

Resources