Mongoose unique index on subdocument - mongodb

Let's say I have a simple schema:
var testSchema = new mongoose.Schema({
map: { type: [ mongoose.Schema.Types.Mixed ], default: [] },
...possibly something else
});
Now let's ensure that pairs (_id, map._id) are unique.
testSchema.index({ _id: 1, 'map._id': 1 }, { unique: true });
Quick check using db.test.getIndexes() shows that it was created.
{
"v" : 1,
"unique" : true,
"key" : {
"_id" : 1,
"map._id" : 1
},
"name" : "_id_1_map._id_1",
"ns" : "test.test",
"background" : true,
"safe" : null
}
The problem is, this index is ignored and I can easily create multiple subdocuments with the same map._id. I can easily execute following query multiple times:
db.maps.update({ _id: ObjectId("some valid id") }, { $push: { map: { '_id': 'asd' } } });
and end up with following:
{
"_id": ObjectId("some valid id"),
"map": [
{
"_id": "asd"
},
{
"_id": "asd"
},
{
"_id": "asd"
}
]
}
What's going on here? Why can I push conflicting subdocuments?

Long story short: Mongo doesn't support unique indexes for subdocuments, although it allows creating them...

This comes up in google so I thought I'd add an alternative to using an index to achieve unique key constraint like functionality in subdocuments, hope that's OK.
I'm not terribly familiar with Mongoose so it's just a mongo console update:
var foo = { _id: 'some value' }; //Your new subdoc here
db.yourCollection.update(
{ '_id': 'your query here', 'myArray._id': { '$ne': foo._id } },
{ '$push': { myArray: { foo } })
With documents looking like:
{
_id: '...',
myArray: [{_id:'your schema here'}, {...}, ...]
}
The key being that you ensure update will not return a document to update (i.e. the find part) if your subdocument key already exists.

First objectId length in mongodb must be 24. Then you can turn off _id, and rename _id as id or others,and try $addToSet. Good luck.
CoffeeScript example:
FromSchema = new Schema(
source: { type: String, trim: true }
version: String
{ _id: false }//to trun off _id
)
VisitorSchema = new Schema(
id: { type: String, unique: true, trim: true }
uids: [ { type: Number, unique: true} ]
from: [ FromSchema ]
)
//to update
Visitor.findOneAndUpdate(
{ id: idfa }
{ $addToSet: { uids: uid, from: { source: source, version: version } } }
{ upsert: true }
(err, visitor) ->
//do stuff

Related

Mongoose / MongoDB - How to push new array element onto correct parent array element

mongodb 3.0.7
mongoose 4.1.12
I want to push new element : "bbb"
onto groups array which lives inside outer orgs array ...
original mongo data from this :
{
orgs: [
{
org: {
_bsontype: "ObjectID",
id: "123456789012"
},
groups: [
"aaa"
]
}
],
_id: {
_bsontype: "ObjectID",
id: "888888888888"
}
}
to this :
{
orgs: [
{
org: {
_bsontype: "ObjectID",
id: "123456789012"
},
groups: [
"aaa",
"bbb"
]
}
],
_id: {
_bsontype: "ObjectID",
id: "888888888888"
}
}
Here is a hardcoded solution yet I do not want
to hardcode array index (see the 0 in : 'orgs.0.groups' )
dbModel.findByIdAndUpdate(
{ _id: ObjectId("888888888888".toHex()), 'orgs.org' : ObjectId("123456789012".toHex()) },
{ $push : { 'orgs.0.groups' : 'bbb'
}
},
{ safe: true,
upsert: false,
new : true
}
)
... I was hoping a simple 'orgs.$.groups' would work, but no. Have also tried 'orgs.groups' , also no.
Do I really need to first retrieve the orgs array, identify the index
then perform some second operation to push onto proper orgs array element ?
PS - suggested duplicate answer does not address this question
Found solution, had to use
dbModel.update
not
dbModel.findOneAndUpdate nor dbModel.findByIdAndUpdate
when using '$' to indicate matched array index in multi-level documents
'orgs.$.groups'
this code works :
dbModel.update(
{ _id: ObjectId("888888888888".toHex()), 'orgs.org' : ObjectId("123456789012".toHex()) },
{ $push : { 'orgs.$.groups' : 'bbb'
}
},
{ safe: true,
upsert: false,
new : true
}
)
I wonder if this is a bug in mongoose ? Seems strange findOneAndUpdate fails to work.

How to check if a field in MongoDB is [] or {}?

MongoDB. One of the fields of a document can be either an array, including an empty array, a subdocument, that can be empty or not, or null, or not exist at all. I need a condition for find() that will match a non-empty subdocument, and only that.
So:
fieldName: {} - no match.
fieldName: [ { id:0 } ] - no match.
fieldName: [ {} ] - no match.
No field called fieldName - no match.
fieldName: null - no match.
fieldName: { id: 0 } - match.
I have no rights to modify anything, I have to work with the database as is. How to formulate that find() ?
Use the following query:
db.test.find({
"fieldName": { "$gt": {} },
"fieldName.0": { "$exists": false }
})
For example, with the above test case, insert the following documents:
db.test.insert([
{ _id: 1, fieldName: {} },
{ _id: 2, fieldName: [ { id: 0 } ] },
{ _id: 3, fieldName: [ {} ] },
{ _id: 4 },
{ _id: 5, fieldName: null},
{ _id: 6, fieldName: { id: 0 } }
])
the above query will return the document with _id: 6
/* 0 */
{
"_id" : 6,
"fieldName" : {
"id" : 0
}
}
You can use the $type and the $exists operator.
The first check the type of id and the latter if fieldname is an array using the so called dot notation.
db.collection.find({
"fieldName.id": { $type: 1 }, "fieldName.0": { $exists: false }
})

How can I find all records where an id is in one array, or another id is in another array?

I need to perform a query that returns all results where an id, or array of ids in an array of ids AND another id, or array of ids, is in another array of ids. Perhaps an example will better explain what I'm trying to do:
Schema:
var somethingSchema = mongoose.Schema({
space_id : String,
title : String,
created : {
type: Date,
default: Date.now
},
visibility : {
groups : [{
type : String,
ref : 'Groups'
}],
users : [{
type : String,
ref : 'User'
}]
}
});
Query:
something.find({
space_id: req.user.space_id,
$and: [
{ $or: [{ "visibility.groups": { $in: groups } }] },
{ $or: [{ "visibility.users": { $in: users } }] }
]
}, function (err, data) {
return res.json(data);
});
In this example, both groups and users are arrays of ids. The query above isn't working. It always returns an empty array. What am I doing wrong?
You should be including all clauses to OR together in a single $or array:
something.find({
space_id: req.user.space_id,
$or: [{ "visibility.groups": { $in: groups } },
{ "visibility.users": { $in: users } }]
}, function (err, data) {
return res.json(data);
});
Which translates to: find all docs with a matching space_id AND that have a visibility.groups value in groups OR a visibility.users value in users.

Mongoose aggregate issue with grouping by nested field

I have a schema that stores user attendance for events:
event:
_id: ObjectId,
...
attendances: [{
user: {
type: ObjectId,
ref: 'User'
},
answer: {
type: String,
enum: ['yes', 'no', 'maybe']
}
}]
}
Sample data:
_id: '533483aecb41af00009a94c3',
attendances: [{
user: '531770ea14d1f0d0ec42ae57',
answer: 'yes',
}, {
user: '53177a2114d1f0d0ec42ae63',
answer: 'maybe',
}],
I would like to return this data in the following format when I query for all attendances for a user:
var attendances = {
yes: ['533497dfcb41af00009a94d8'], // These are the event IDs
no: [],
maybe: ['533497dfcb41af00009a94d6', '533497dfcb41af00009a94d2']
}
I am not sure the aggegation pipeline will return it in this format? So I was thinking I could return this and modify it easily:
var attendances = [
answer: 'yes',
ids: ['533497dfcb41af00009a94d8'],
},{
answer: 'no',
ids: ['533497dfcb41af00009a94d8']
}, {
answer: 'maybe',
ids: ['533497dfcb41af00009a94d6', '533497dfcb41af00009a94d2']
}];
My attempt is not succesful however. It doesn't group it by answer:
this.aggregate({
$match: {
'attendances.user': someUserId
}
}, {
$project: {
answer: '$attendances.answer'
}
}, {
$group: {
_id: '$answer',
ids: {
$addToSet: "$_id"
}
}
}, function(e, docs) {
});
Can I return the data I need in the first desired format and if not how can I correct the above code to achieve the desired result?
On that note - perhaps the map-reduce process would be better suited?
The below query will help you get close to the answer you want. Although, it isn't exactly in the same format you are expecting, you get separate documents for each answer option and an array of event id's.
db.collection.aggregate([
// Unwind the 'attendances' array
{"$unwind" : "$attendances"},
// Match for user
{"$match" : {"attendances.user" : "53177a2114d1f0d0ec42ae63"}},
// Group by answer and push the event id's to an array
{"$group" : {_id : "$attendances.answer", eventids : {$push : "$_id"}}}
])
This produces the below output:
{
"result" : [
{
"_id" : "yes",
"eventids" : [
ObjectId("53d968a8c4840ac54443a9d6")
]
},
{
"_id" : "maybe",
"eventids" : [
ObjectId("53d968a8c4840ac54443a9d7"),
ObjectId("53d968a8c4840ac54443a9d8")
]
}
],
"ok" : 1
}

way to update multiple documents with different values

I have the following documents:
[{
"_id":1,
"name":"john",
"position":1
},
{"_id":2,
"name":"bob",
"position":2
},
{"_id":3,
"name":"tom",
"position":3
}]
In the UI a user can change position of items(eg moving Bob to first position, john gets position 2, tom - position 3).
Is there any way to update all positions in all documents at once?
You can not update two documents at once with a MongoDB query. You will always have to do that in two queries. You can of course set a value of a field to the same value, or increment with the same number, but you can not do two distinct updates in MongoDB with the same query.
You can use db.collection.bulkWrite() to perform multiple operations in bulk. It has been available since 3.2.
It is possible to perform operations out of order to increase performance.
From mongodb 4.2 you can do using pipeline in update using $set operator
there are many ways possible now due to many operators in aggregation pipeline though I am providing one of them
exports.updateDisplayOrder = async keyValPairArr => {
try {
let data = await ContestModel.collection.update(
{ _id: { $in: keyValPairArr.map(o => o.id) } },
[{
$set: {
displayOrder: {
$let: {
vars: { obj: { $arrayElemAt: [{ $filter: { input: keyValPairArr, as: "kvpa", cond: { $eq: ["$$kvpa.id", "$_id"] } } }, 0] } },
in:"$$obj.displayOrder"
}
}
}
}],
{ runValidators: true, multi: true }
)
return data;
} catch (error) {
throw error;
}
}
example key val pair is: [{"id":"5e7643d436963c21f14582ee","displayOrder":9}, {"id":"5e7643e736963c21f14582ef","displayOrder":4}]
Since MongoDB 4.2 update can accept aggregation pipeline as second argument, allowing modification of multiple documents based on their data.
See https://docs.mongodb.com/manual/reference/method/db.collection.update/#modify-a-field-using-the-values-of-the-other-fields-in-the-document
Excerpt from documentation:
Modify a Field Using the Values of the Other Fields in the Document
Create a members collection with the following documents:
db.members.insertMany([
{ "_id" : 1, "member" : "abc123", "status" : "A", "points" : 2, "misc1" : "note to self: confirm status", "misc2" : "Need to activate", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") },
{ "_id" : 2, "member" : "xyz123", "status" : "A", "points" : 60, "misc1" : "reminder: ping me at 100pts", "misc2" : "Some random comment", "lastUpdate" : ISODate("2019-01-01T00:00:00Z") }
])
Assume that instead of separate misc1 and misc2 fields, you want to gather these into a new comments field. The following update operation uses an aggregation pipeline to:
add the new comments field and set the lastUpdate field.
remove the misc1 and misc2 fields for all documents in the collection.
db.members.update(
{ },
[
{ $set: { status: "Modified", comments: [ "$misc1", "$misc2" ], lastUpdate: "$$NOW" } },
{ $unset: [ "misc1", "misc2" ] }
],
{ multi: true }
)
Suppose after updating your position your array will looks like
const objectToUpdate = [{
"_id":1,
"name":"john",
"position":2
},
{
"_id":2,
"name":"bob",
"position":1
},
{
"_id":3,
"name":"tom",
"position":3
}].map( eachObj => {
return {
updateOne: {
filter: { _id: eachObj._id },
update: { name: eachObj.name, position: eachObj.position }
}
}
})
YourModelName.bulkWrite(objectToUpdate,
{ ordered: false }
).then((result) => {
console.log(result);
}).catch(err=>{
console.log(err.result.result.writeErrors[0].err.op.q);
})
It will update all position with different value.
Note : I have used here ordered : false for better performance.