mongo near set maxDistance as value in collection - javascript

I am sorry by the title. I was hard to describe. I have this collection
{
"_id" : ObjectId("55cb9c666c522cafdb053a68"),
location: {
type: "Point",
coordinates: [-73.856077, 40.848447]
},
"maxDistancevalue" : 100000
}
Now I want to find of the current location is within: 100000 as defined is the collection by "maxDistancevalue"
The code will be like this. But how set the maxDistancevalue?
db.places.find(
{
location:
{ $near :
{
$geometry: { type: "Point", coordinates: [ -73.9667, 40.78 ] },
$minDistance: **??????, -->maxDistancevalue**
$maxDistance: 0
}
}
}
)

You can use Aggregation Framework's $geoNear pipeline stage to reference other existing field. It requires 2dsphere index to be created on your collection, so start with:
db.places.createIndex({location:"2dsphere"});
and then you can run your aggregate() query:
db.places.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [ -73.9667, 40.78 ] },
distanceField: "dist.calculated",
maxDistance: "$maxDistancevalue"
}
}
])

Related

$divide is mongoose is giving "Cast to Number failed for value"

Here i am trying to update the 'rating' field of my document by taking average of previously existing value of the rating field and newly sent rating value.
this is my rating field specification in the model
rating: {
type: Number,
min: 0,
max: 5,
required: true,
},
this is my request body and controller function
const { newRating, bookID, userName, comment } = req.body;
const updateRating = await Book.findByIdAndUpdate(
{ _id: bookID },
{
rating: { $divide: [{ $inc: { rating: Number(newRating) } }, 2] },
$inc: { numOfRatings: 1 },
},
{ new: true }
);
and i am using postman to send client side data
here for example the rating field has previously set value of 4.1 and i am sending 5 as new rating in req.body then i want the rating field to have an updated value of 4.55 ((4.1+5)/2)
and this is the output i am getting in postman
{
"message": "Cast to Number failed for value "{ '$divide': [ { '$inc': [Object] }, 2 ] }" (type Object) at path "rating"",
"stack": "CastError: Cast to Number failed for value "{ '$divide': [ { '$inc': [Object] }, 2 ] }" (type Object) at path "rating"\n at model.Query.exec (D:\Programs\VS Code\Web Development\lmsbackend\node_modules\mongoose\lib\query.js:4891:21)\n at model.Query.Query.then (D:\Programs\VS Code\Web Development\lmsbackend\node_modules\mongoose\lib\query.js:4990:15)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)"
}
i tried few things seeing mongodb solutions but it is not working out for me. Thank you in advance.
I hope this will work:
const updateRating = await Book.findByIdAndUpdate(
{ _id: bookID },
[{
"$set": {
"rating": { "$divide": [{ "$sum": ["$rating", 5] }, 2] },
"numOfRatings": { "$sum": ["$numOfRatings", 1 ] }
}
}],
{ new: true }
);
$divide is only available for Aggregation framework, so you need to change your update (second) input like this:
await Book.findByIdAndUpdate({
_id: bookID
},
[
{
"$set": {
"rating": { "$divide": [{ "$sum": ["$rating", Number(newRating)] }, 2] },
"numOfRatings": { "$sum": ["$numOfRatings", 1 ] }
}
}
])
Working example

how to query in mongodb with loop in same collection, until i find null or empty value?

I have stacked in a nested object. here is my collection.
{
"key": 1,
"subKey": ""
},
{
"key": 2,
"subKey": 1
},
{
"key": 3,
"subKey": 2
},
{
"key": 4,
"subKey": 3
}
I want to query Key:4, which gives me result
{
"key": 4,
"subKey": 3
}
after getting result i want to query "subKey": 3 as a key:"$subKey" and i want to run a loop, until i find a empty subKey in our case It is Key:1. and whenever i found an empty subKey i want it document as a parent.
In the end, I want the result
{
"key": 4,
"parent":{"key":1,"subKey":"",....}
}
or similar.
Is it possible by using MongoDB built-in function? if not available how do I achieve this goal?
also, I want an alternative solution for it if there is.
You can achieve using $graphLookup
play
db.collection.aggregate([
{
$graphLookup: {
from: "collection",
startWith: "$key",
connectFromField: "subKey",
connectToField: "key",
as: "keys"
}
}
])
If you want a match filter add it,
play
db.collection.aggregate([
{
$match: {
key: 4
}
},
{
$graphLookup: {
from: "collection",
startWith: "$key",
connectFromField: "subKey",
connectToField: "key",
as: "keys"
}
}
])
Important consideration:
The $graphLookup stage must stay within the 100 MiB memory limit. If allowDiskUse: true is specified for the aggregate() operation, the $graphLookup stage ignores the option
To transform the data, you cannot have duplicate keys in parent object. So parent should be an array
play
db.collection.aggregate([
{
$match: {
key: 4
}
},
{
$graphLookup: {
from: "collection",
startWith: "$key",
connectFromField: "subKey",
connectToField: "key",
as: "keys"
}
},
{
"$addFields": {
"parent": {
"$map": {
"input": "$keys",
"as": "res",
"in": {
"key": "$$res.key",
"subKey": "$$res.subKey"
}
}
},
"key": "$key",
}
},
{
$project: {
keys: 0
}
}
])

Mongoose can't recognize my 2dsphere index

I'm trying to add 2dSphere index for my field startLocation exists in tourSchema. Here how it looks below
startLocation: {
type: {
type: String,
default: 'Point',
enum: ['Point']
},
coordinates: [Number],
address: String,
description: String
}
And you can also see below what and how I've added indexes on this Schema
tourSchema.index({price:1,ratingsAverage:-1});
tourSchema.index({slug:1});
tourSchema.index({ startLocation: '2dsphere' });
Unfortunately Mongodb can't recognize startLocation index. Using Mongo Compass , I'm able to see all indexes that I've created except startLocation:'2dsphere'.
Here is the error that postman gives me below when I send request to getDistances method in controller:
{
"status": "error",
"error": {
"operationTime": "6791492473605586945",
"ok": 0,
"errmsg": "$geoNear requires a 2d or 2dsphere index, but none were found",
"code": 27,
"codeName": "IndexNotFound",
"$clusterTime": {
"clusterTime": "6791492473605586945",
"signature": {
"hash": "4LYCSBslSoLAoqj93bLXmpubBxs=",
"keyId": "6779443539857113090"
}
},
"name": "MongoError",
"statusCode": 500,
"status": "error"
},
"message": "$geoNear requires a 2d or 2dsphere index, but none were found",
"stack": "MongoError: $geoNear requires a 2d or 2dsphere index, but none were found\n at Connection.<anonymous> (C:\\Users\\timuc\\Downloads\\starter\\starter\\node_modules\\mongodb-core\\lib\\connection\\pool.js:443:61)\n at Connection.emit (events.js:223:5)\n at processMessage (C:\\Users\\timuc\\Downloads\\starter\\starter\\node_modules\\mongodb-core\\lib\\connection\\connection.js:364:10)\n at TLSSocket.<anonymous> (C:\\Users\\timuc\\Downloads\\starter\\starter\\node_modules\\mongodb-core\\lib\\connection\\connection.js:533:15)\n at TLSSocket.emit (events.js:223:5)\n at addChunk (_stream_readable.js:309:12)\n at readableAddChunk (_stream_readable.js:290:11)\n at TLSSocket.Readable.push (_stream_readable.js:224:10)\n at TLSWrap.onStreamRead (internal/stream_base_commons.js:181:23)"
}
I tried to add point: '2dsphere' which was recognized by mongodb but I'm not satisfied. Because when I send request to method in controller that returns success but with empty array.
Here is the method which was triggered in controller:
exports.getDistances = catchAsync(async (req, res, next) => {
const { latlng, unit } = req.params;
const [lat, lng] = latlng.split(",");
if (!lat || !lng) {
new AppError( "Please provide latitude and longitude in the format lat,lng", 400);
}
const distances = await Tour.aggregate([
{
$geoNear: {
near: {
type: "Point",
coordinates: [lng * 1, lat * 1]
},
distanceField: "distance"
}
}
]);
res.status(200).json({
status: "success",
data: {
data: distances
}
});
});
also from router you can see how I send the request URL below
tourRouter.route('/distances/:latlng/unit/:unit').get(tourController.getDistances);
I strongly believe that you are not using the proper collection. This is working for MongoDB 4.2.
Creating the index:
db.location.createIndex({
startLocation: "2dsphere"
})
Indexes of that collection:
db.location.getIndexes()
[{
"v": 2,
"key": {
"_id": 1
},
"name": "_id_",
"ns": "stackoverflow.location"
}, {
"v": 2,
"key": {
"startLocation": "2dsphere"
},
"name": "startLocation_2dsphere",
"ns": "stackoverflow.location",
"2dsphereIndexVersion": 3
}
]
Inserting some data:
db.location.insert({
startLocation: {
type: "Point",
coordinates: [40, 5],
address: "Hellostreet 1",
description: "Hello"
}
})
Aggregate the collection:
db.location.aggregate([{
$geoNear: {
near: {
type: 'Point',
coordinates: [41, 6]
},
distanceField: 'distance'
}
}
])
The result:
{
"_id" : ObjectId("5e404cdd13552bde0a0a9dc5"),
"startLocation" : {
"type" : "Point",
"coordinates" : [
40,
5
],
"address" : "Hellostreet 1",
"description" : "Hello"
},
"distance" : 157065.62445348964
}

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.

MongoDB Match and Slice multiple child arrays

I currently have this schema
var dataSchema = new Schema({
hid: { type: String },
sensors: [{
nid: { type: String },
sid: { type: String },
data: {
param1: { type: String },
param2: { type: String },
data: { type: String }
},
date: { type: Date, default: Date.now }
}],
actuators: [{
nid: { type: String },
aid: { type: String },
control_id: { type: String },
data: {
param1: { type: String },
param2: { type: String },
data: { type: String }
},
date: { type: Date, default: Date.now }
}],
status: [{
nid: {type: String},
status_code: {type: String},
date: { type: Date, default: Date.now }
}],
updated: { type: Date, default: Date.now },
created: { type: Date }
});
The query that I'm trying to build should search the schema by "hid", then only pick from the "sensors", "actuators" and "status" arrays the objects that match the nid that i provide and then also limit the result to 10 element for each array.
Actually, don't use multiple pipeline phases when "one" will do. Every pipeline stage you include is effectively adding "time" to processing since it's another pass through the data.
So what logically works in a "single" stage, should stay in a "single" stage:
Data.aggregate([
{ "$match": { "hid": hid } },
{ "$project": {
"sensors": {
"$slice": [
{ "$filter": {
"input": "$sensors",
"as": "sensor",
"cond": { "$eq": [ "$$sensor.nid", nid ] }
}},
-10
]
},
"actuators": {
"$slice": [
{ "$filter": {
"input": "$actuators",
"as": "actuator",
"cond": { "$eq": [ "$$actuator.nid", nid ] }
}},
-10
]
},
"status": {
"$slice": [
{ "$filter": {
"input": "$status",
"as": "status",
"cond": { "$eq": [ "$$status.nid", nid ] }
}},
-10
]
},
"updated": 1,
"created": 1
}}
])
Also, it's not necessary to use "_id": 1 in inclusions since "_id" is always included unless explicitly "excluded".
The main case is to try not to create unnecessary stages, since it's bad for performance. Good indicators of this are:
$project followed by $project, usually means you can do this in one stage.
$project followed by $group is probably going to get compacted by the "optimizer" anyway, but you "should" get in the habit of of combining the two.
$project followed by $match, should indicate that you probably should be using $redact in a single stage instead. Since you likely did the $project to produce fields based on calculations, that were then considered in the $match.
And finally:
$unwind before a $match, really should have been a $filter in a $project "before" the $unwind. Since it does in fact work a lot faster to "filter" within the document, and also saves cost on processing the $unwind, due to less output from already filtered content.
But one more:
All of the "array" operations such as $map, $filter, $slice and even the "set operators" should always be used "in-line" with each other, in cases where those are manipulating the same array content. The same now even applies to wrapping with $sum or $max now that these can work directly on arrays themselves, and often with the strange looking but effective:
{ "$sum": { "$sum": "$array" } }
structure within a $group stage.
This can be achieved with the aggregation framework.
The query look like this:
Data.aggregate([
{ "$match": { "hid": hid } },
{ "$project": {
"_id": 1,
"sensors": {
"$filter": { "input": "$sensors", "as": "sensor", "cond": { "$eq": [ "$$sensor.nid", nid ] } }
},
"actuators": {
"$filter": { "input": "$actuators", "as": "actuator", "cond": { "$eq": [ "$$actuator.nid", nid ] } }
},
"status": {
"$filter": { "input": "$status", "as": "state", "cond": { "$eq": [ "$$state.nid", nid ] } }
},
"updated": 1,
"created": 1
}},
{ "$project": {
"_id": 1,
"sensors": {
"$slice": [ "$sensors", -10 ]
},
"actuators": {
"$slice": [ "$actuators", -10 ]
},
"status": {
"$slice": [ "$status", -10 ]
},
"updated": 1,
"created": 1
}}
]).exec(function(err,data) {
});
It uses the $match to find the schema, the $filter to pick from the array only the elements that match the provided nid and then uses the $slice to pick the last 10 elements from the filtered array

Categories

Resources