I'm new to mongodb, and I'm using mongoose to validate and order the data (I'm open to change it to MySQL if this doesn't work).
The app will be an e-shop, to buy merchandising related to movies, games, ext.
My schema is as follows:
var productSchema = {
id: {
type: String,
required: true
},
name: {
type: String,
required: true
},
img: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
stock: {
type: Number,
required: true
},
category: {
object: {
type: String,
required: true
},
group: {
type: String,
required: true
},
name: {
type: String,
required: true
}
}
};
This is what I would like to do:
If I have the following data in category:
category.object = "ring"
category.group = "movies"
category.name= "lord of the rings"
I want the id to be made of the first letters of every field in category and a number (the number of the last item added plus 1). In this case, It would be RMLOTR1.
What I'm doing right now
I'm adding a lot of data at the same time, so every time I do it, I made a function that iterates through all the items added and does what I want but...
My question is
Is there a built-in way to do this with mongodb or mongoose, adding the data and creating the id at the same time? I know I can do a virtual, but I want the data to be stored.
Extras
If it's not posible to do this with mongodb, is there a way to do this with MySQL?
Is doing this kind of thing considered a correct/wrong approach?
You are basically looking for a "pre" middleware hook on the "save" event fired by creating new documents in the collection. This will inspect the current document content and extract the "strings" from values in order to create your "prefix" value for _id.
There is also another part, where the "prefix" needs the addition of the numeric counter when there is already a value present for that particular "prefix" to make it distinct. There is a common technique in MongoDB used to "Generate an auto-incrementing sequence field", which basically involves keeping a "counters" collection and incrementing the value each time you access it.
As a complete and self contained demonstration, you combine the techniques as follows:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/warehouse');
var counterSchema = new Schema({
"type": { "type": String, "required": true },
"prefix": { "type": String, "required": true },
"counter": Number
});
counterSchema.index({ "type": 1, "prefix": 1 },{ "unique": true });
counterSchema.virtual('nextId').get(function() {
return this.prefix + this.counter;
});
var productSchema = new Schema({
"_id": "String",
"category": {
"object": { "type": String, "required": true },
"group": { "type": String, "required": true },
"name": { "type": String, "required": true }
}
},{ "_id": false });
productSchema.pre('save', function(next) {
var self = this;
if ( !self.hasOwnProperty("_id") ) {
var prefix = self.category.object.substr(0,1).toUpperCase()
+ self.category.group.substr(0,1).toUpperCase()
+ self.category.name.split(" ").map(function(word) {
return word.substr(0,1).toUpperCase();
}).join("");
mongoose.model('Counter').findOneAndUpdate(
{ "type": "product", "prefix": prefix },
{ "$inc": { "counter": 1 } },
{ "new": true, "upsert": true },
function(err,counter) {
self._id = counter.nextId;
next(err);
}
);
} else {
next(); // Just skip when _id is already there
}
});
var Product = mongoose.model('Product',productSchema),
Counter = mongoose.model('Counter', counterSchema);
async.series(
[
// Clean data
function(callback) {
async.each([Product,Counter],function(model,callback) {
model.remove({},callback);
},callback);
},
function(callback) {
async.each(
[
{
"category": {
"object": "ring",
"group": "movies",
"name": "lord of the rings"
}
},
{
"category": {
"object": "ring",
"group": "movies",
"name": "four weddings and a funeral"
}
},
{
"category": {
"object": "ring",
"group": "movies",
"name": "lord of the rings"
}
}
],
function(data,callback) {
Product.create(data,callback)
},
callback
)
},
function(callback) {
Product.find().exec(function(err,products) {
console.log(products);
callback(err);
});
},
function(callback) {
Counter.find().exec(function(err,counters) {
console.log(counters);
callback(err);
});
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
)
This gives you output like:
[ { category: { name: 'lord of the rings', group: 'movies', object: 'ring' },
__v: 0,
_id: 'RMLOTR1' },
{ category:
{ name: 'four weddings and a funeral',
group: 'movies',
object: 'ring' },
__v: 0,
_id: 'RMFWAAF1' },
{ category: { name: 'lord of the rings', group: 'movies', object: 'ring' },
__v: 0,
_id: 'RMLOTR2' } ]
[ { __v: 0,
counter: 2,
type: 'product',
prefix: 'RMLOTR',
_id: 57104cdaa774fcc73c1df0e8 },
{ __v: 0,
counter: 1,
type: 'product',
prefix: 'RMFWAAF',
_id: 57104cdaa774fcc73c1df0e9 } ]
To first understand the Counter schema and model, you are basically defining something where you are going to look up a "unique" key and also attach a numeric field to "increment" on match. For convenience this just has a two fields making up the unique combination and a compound index defined. This could just also be a compound _id if so wanted.
The other convenience is the virtual method of nextId, which just does a concatenation of the "prefix" and "counter" values. It's also best practice here to include something like "type" here since your Counter model can be used to service "counters" for use in more than one collection source. So here we are using "product" whenever accessing in the context of the Product model to differentiate it from other models where you might also keep a similar sequence counter. Just a design point that is worthwhile following.
For the actual Product model itself, we want to attach "pre save" middleware hook in order to fill the _id content. So after determining the character portion of the "prefix", the operation then goes off and looks for that "prefix" with the "product" type data in combination in the Counter model collection.
The function of .findOneAndUpdate() is to look for a document matching the criteria in the "counters" collection and then where a document is found already it will "increment" the current counter value by use of the $inc update operator. If the document was not found, then the "upsert" option means that a new document will be created, and at any rate the same "increment" will happen in the new document as well.
The "new" option here means that we want the "modified" document to be returned ( either new or changed ) rather than what the document looked like before the $inc was applied. The result is that "counter" value will always increase on every access.
Once that is complete and a document for Counter is either incremented or created for it's matching keys, then you now have something you can use to assign to the _id in the Product model. As mentioned earlier you can use the virtual here for convenience to get the prefix with the appended counter value.
So as long as your documents are always created by either the .create() method from the model or by using new Product() and then the .save() method, then the methods attached to your "model" in your code are always executed.
Note here that since you want this in _id, then as a primary key this is "immutable" and cannot change. So even if the content in the fields referenced was later altered, the value in _id cannot be changed, and therefore why the code here makes no attempt when an _id value is already set.
Related
In my document i have array of images in which each image has url and public_id.
I want only the url public_id but addditional parameter of _id is also stored. Which i want to avoid.
Stored in the database as:
"images": [
{
"url": "https://res.cloudinary.com/dpurb6xes/image/upload/v1665806337/vivans/po0zh7eots60azad3y5b.png",
"public_id": "vivans/po0zh7eots60azad3y5b",
"_id": "634a4db7177280021c56737c"
},
{
"url": "https://res.cloudinary.com/dpurb6xes/image/upload/v1665806337/vivans/po0zh7eots60azad3y5b.png",
"public_id": "vivans/po0zh7eots60azadasdb",
"_id": "634a4db7177280021c56737d"
}
],
Mongoose Schema
const imageArray = new mongoose.Schema({
url: { type: String },
public_id: { type: String },
});
const productSchema = new mongoose.Schema({
images: [imageArray],
});
My Post request
{
"images": [
{
"url": "https://res.cloudinary.com/dpurb6xes/image/upload/v1665806337/vivans/po0zh7eots60azad3y5b.png",
"public_id": "vivans/po0zh7eots60azad3y5b"
},
{
"url": "https://res.cloudinary.com/dpurb6xes/image/upload/v1665806337/vivans/po0zh7eots60azad3y5b.png",
"public_id": "vivans/po0zh7eots60azadasdb"
}
],
}
How to get rid of _id stored in the database.
The Schema class can receive a second parameter, which is used to pass some options, one of them is "_id", which is a boolean value, so you just have to provide this second parameter with the _id property as false in order to avoid create the _id property in your documents.
This way:
const imageArray = new mongoose.Schema({
url: { type: String },
public_id: { type: String },
}, { _id: false }
);
You can look the entire options list in the official docs: Mongoose Schema - options
Im just learning indexing with Mongoose/MongoDB and I dont know why this isnt working.
this is my schema
const timeSchema = new mongoose.Schema({
actionId:{
type:String,
required:true
},
start: {
type: Date
},
end: {
type: Date
},
user:{type : mongoose.Schema.Types.ObjectId, ref : 'User'},
task:{type : mongoose.Schema.Types.ObjectId, ref : 'Task'},
pausedSeconds:{
type: Number,
default: 0
}
});
const Time = mongoose.model('Time', timeSchema)
i want to have a text index in two populated fields user and task, i created the index this way
timeSchema.index({"user.name":"text","task.taskName":"text"})
Here is an example of the documents
{
"pausedSeconds": 18,
"_id": "5db1dde8d5bc93526c26fa38",
"actionId": "5feaebcf-6b90-45be-8104-452d643472a0",
"user": {
"_id": "5d4af77e4b6cbf3dd8c5f3ac",
"name": "admin"
},
"task": {
"_id": "5d4aff2f61ad755154b8a1c6",
"taskName": "task 1 updated!"
},
"start": "2019-10-24T17:22:48.000Z",
"end": "2019-10-24T17:30:00.000Z"
},
I have one issue and one question
The issue is:
What im trying to do is get all the documents that have "task 1 updated" (for task.taskName) or
"admin" (for user.name) doing it this way
Time.find({ '$text': { '$search': "admin" } })
Time.find({ '$text': { '$search': "task 1 updated" } })
but it doesnt seem to work
The question is:
If I want to do a text search for the fields start,end being a Date type or for the field pausedSeconds being a Number type what should I do?
Thanks in advance
In your query, you aren't specifying what property to search on. Do this: Time.find({taskName: { '$text': { '$search': "admin" }}}).
Also, I'm not sure if you're just not showing all the code or if you're actually doing your query wrong, but it should be written like this:
Time.find({taskName: { '$text': { '$search': "admin" }}}).exec(function(err, times) {
if(err) return console.log(err);
console.log(times);
});
I am using NodeJs and Mongoose and building a feature to list near by deals.
Deal.db.db.command({
"geoNear": Deal.collection.name,
"near": [23,67],
"spherical": true,
"distanceField": "dis"
}, function (err, documents) {
if (err) {
return next(err);
}
console.log(documents);
});
Deal schema:
var dealSchema = new mongoose.Schema({
title: String,
merchant: {
type: mongoose.Schema.Types.ObjectId,
ref: Merchant,
index: true
}
});
Here, I get all deals and their distances from current location. Inside Deal schema I have a merchant as reference object.
How do I populate merchants with each returned Deal object?
Do I need to iterate through all returned Deal objects and populate manually?
This actually turns out to be kind of interesting. Certainly what you do not want to really do is "dive into" the native driver part, though you possibly could on solution 2. Which leads to that there are a few ways to go about this without actually rolling your own queries to match this manually.
First a little setup. Rather than reproduce your dataset I have just picked something I had lying around from previous testing. The principles are the same, so a basic setup:
var mongoose = require('mongoose'),
async = require('async'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost');
var infoSchema = new Schema({
"description": String
});
var shapeSchema = new Schema({
"_id": String,
"amenity": String,
"shape": {
"type": { "type": String },
"coordinates": []
},
"info": { "type": Schema.Types.ObjectId, "ref": "Info" }
});
var Shape = mongoose.model( "Shape", shapeSchema );
var Info = mongoose.model( "Info", infoSchema );
Essentially one model with the "geo" information and the other that is referenced with just some information we want to populate. Also my lazy alteration of data:
{
"_id" : "P1",
"amenity" : "restaurant",
"shape" : { "type" : "Point", "coordinates" : [ 2, 2 ] }
}
{
"_id" : "P3",
"amenity" : "police",
"shape" : { "type" : "Point", "coordinates" : [ 4, 2 ] }
}
{
"_id" : "P4",
"amenity" : "police",
"shape" : { "type" : "Point", "coordinates" : [ 4, 4 ] }
}
{
"_id" : "P2",
"amenity" : "restaurant",
"shape" : { "type" : "Point", "coordinates" : [ 2, 4 ] },
"info" : ObjectId("539b90543249ff8d18e863fb")
}
So there is one thing in there that we can expect to populate. As it turns out a $near query is pretty straightforward as does sort as expected:
var query = Shape.find(
{
"shape": {
"$near": {
"$geometry": {
"type": "Point",
"coordinates": [ 2, 4 ]
}
}
}
}
);
query.populate("info").exec(function(err,shapes) {
if (err) throw err;
console.log( shapes );
});
That is just a standard .find() with the $near operator invoked. This is MongoDB 2.6 syntax, so there is that. But the results sort correctly and populate as expected:
[
{ _id: 'P2',
amenity: 'restaurant',
info:
{ _id: 539b90543249ff8d18e863fb,
description: 'Jamies Restaurant',
__v: 0 },
shape: { type: 'Point', coordinates: [ 2, 4 ] } },
{ _id: 'P4',
amenity: 'police',
info: null,
shape: { type: 'Point', coordinates: [ 4, 4 ] } },
{ _id: 'P1',
amenity: 'restaurant',
info: null,
shape: { type: 'Point', coordinates: [ 2, 2 ] } },
{ _id: 'P3',
amenity: 'police',
info: null,
shape: { type: 'Point', coordinates: [ 4, 2 ] } }
]
That is pretty nice, and a simple way to invoke. The catch? Sadly changing the operator to $geoNear to take the spherical geometry into account starts throwing errors. So if you want that then you can't just do things as you are used to.
Another approach though is that mongoose has a .geoNear() function that is supported. But just like the db.command invocation, this does not return a mongoose document or other Model type object that will accept .populate(). Solution? Just play with the output a little:
var query = Shape.geoNear({
"type": "Point",
"coordinates": [ 2, 4 ]
},{spherical: true},function(err,shapes) {
if (err) throw err;
shapes = shapes.map(function(x) {
delete x.dis;
var a = new Shape( x.obj );
return a;
});
Shape.populate( shapes, { path: "info" }, function(err,docs) {
if (err) throw err;
console.log( docs );
});
So here the result returned is an array of raw objects. But with a little manipulation you can turn these into something that is going to work with the .populate() method which can also be invoked from the model class as shown.
The result of course is the same, though the field order may be a little different. And you didn't need to iterate an call the queries yourself. This is really all that .populate() is actually doing, but I think we can agree that using the .populate() method at least looks a lot cleaner and does not re-invent the wheel for this purpose.
Does anyone know if it's possible to populate a list of IDs for another model using waterline associations? I was trying to get the many-to-many association working but I don't think it applies here since one side of the relationship doesn't know about the other. Meaning, a user can be a part of many groups but groups don't know which users belong to them. For example, I'm currently working with a model with data in mongodb that looks like:
// Group
{
_id: group01,
var: 'somedata',
},
{
_id: group02,
var: 'somedata',
},
{
_id: group03,
var: 'somedata',
}
// User
{
_id: 1234,
name: 'Jim',
groups: ['group01', 'group03']
}
And I'm trying to figure out if it's possible to setup the models with an association in such a way that the following is returned when querying the user:
// Req: /api/users/1234
// Desired result
{
id: 1234,
name: 'Jim',
groups: [
{
_id: group01,
var: 'somedata',
},
{
_id: group03,
var: 'somedata',
}
]
}
Yes, associations are supported in sails 0.10.x onwards. Here is how you can setup the models
Here is how your user model will look like:
// User.js
module.exports = {
tableName: "users",
attributes: {
name: {
type: "string",
required: true
},
groups: {
collection: "group",
via: "id"
}
}
};
Here is how your group model will look like:
// Group.js
module.exports = {
tableName: "groups",
attributes: {
name: {
type: "string",
required: "true"
}
}
};
Setting up models like this will create three tables in your DB:
users,
groups and
group_id__user_group
The last table is created by waterline to save the associations. Now go on and create groups. Once groups are created, go ahead and create user.
Here is a sample POST request for creation a new user
{
"name": "user1",
"groups": ["547d84f691bff6663ad08147", "547d850c91bff6663ad08148"]
}
This will insert data into the group_id__user_group in the following manner
{
"_id" : ObjectId("547d854591bff6663ad0814a"),
"group_id" : ObjectId("547d84f691bff6663ad08147"),
"user_groups" : ObjectId("547d854591bff6663ad08149")
}
/* 1 */
{
"_id" : ObjectId("547d854591bff6663ad0814b"),
"group_id" : ObjectId("547d850c91bff6663ad08148"),
"user_groups" : ObjectId("547d854591bff6663ad08149")
}
The column user_groups is the user id. And group_id is the group id. Now if you fetch the user using GET request, your response will look like this:
{
"groups": [
{
"name": "group1",
"createdAt": "2014-12-02T09:23:02.510Z",
"updatedAt": "2014-12-02T09:23:02.510Z",
"id": "547d84f691bff6663ad08147"
},
{
"name": "group2",
"createdAt": "2014-12-02T09:23:24.851Z",
"updatedAt": "2014-12-02T09:23:24.851Z",
"id": "547d850c91bff6663ad08148"
}
],
"name": "user1",
"createdAt": "2014-12-02T09:24:21.182Z",
"updatedAt": "2014-12-02T09:24:21.188Z",
"id": "547d854591bff6663ad08149"
}
Please note that groups are not embedded in the user collection. Waterline does the fetch from groups, users and group_id__user_group to show this result to you.
Also, if you want to do this in your controller, you will need to execute like this
User.findOne({'id': "547d854591bff6663ad08149"})
.populate('groups')
.exec(function (err, user){
// handle error and results in this callback
});
Without populate('groups'), you won't get the groups array. Hope this serves your purpose
I have this schema defined:
var bookCollection = new mongoose.Schema({
book:[{
bookTitle: String,
bookOrder: Number,
bookChapters: [{
chapterTitle: String,
chapterIntro: String,
chapterOrder: Number,
chapterArticles: [{
articleTitle: String,
articleIntro: String,
articleOrder: Number,
articleHeadings: [{
headingTitle: String,
headingOrder: Number
}]
}]
}]
}]
});
var bookModel = mongoose.model('bookModel', bookCollection);
I then saved 1 document to mongoDB, this is the JSON object when checking using db.bookmodels.find()
{
"_id": ObjectId("530cc92710f774355434b394"),
"book": [
{
"bookTitle": "Javascript",
"bookOrder": 300,
"_id": ObjectId("530cc92710f774355434b395"),
"bookChapters": [
{
"chapterTitle": "Functions",
"chapterIntro": "All about javascript functions",
"chapterOrder": 500,
"_id": ObjectId("530cc92710f774355434b396"),
"chapterArticles": [
{
"articleTitle": "A visual illustration of the JS function",
"articleIntro": "Something to see here, check it out",
"articleOrder": 500,
"_id": ObjectId("530cc92710f774355434b397"),
"articleHeadings": [
{
"headingTitle": "Parts of a function",
"headingOrder": 500,
"_id": ObjectId("530cc92710f774355434b398")
}
]
}
]
}
]
}
],
"__v": 0
}
If i want to change headingOrder to 100 instead of 500, how would i update the database using mongoose.js? I've been trying several things and i can't seem to get my head around it.
Everywhere you see examples with simple schema's but never with complex schema's like this one.
thx.
You can always load the document in memory, made modifications, i.e. book[0].bookChapters[0].chapterArticles[0].articleHeadings[0].headingOrder = 100 then save() it.
The example at the link above does exactly that, document.find() followed by save().