Sequelize automatically save association objects when parent is saved - node.js

Given the following model:
var Project = sequelize.define('project', {/* ... */})
var Task = sequelize.define('task', {/* ... */})
Project.hasMany(Task, {as: 'Tasks'});
Task.belongsTo(Project);
How can make changes to tasks and have the database updated when I save the project.
Project.findById(1, {include: Task, as: 'tasks'}).then(function(project) {
project.tasks.each(function(task) {
task.status = 'done';
});
project.status = 'closed';
project.updatedBy = 'user';
// ...many more statements that modify the project and tasks for example
project.save(); // << I want this to save the whole hierarchy to the database, similar to how Hibernate does it, if some people are familiar
});
When project.save() is called, the tasks are not updated. Why?

If you use find or findAll you can take advantage of eager-loading as described in Eager loading.
To do this, you need to use the attribute include like: include: [Task]
This way you have an inner join and don't need to query for the tasks.
Based on your code above, you can have:
(I can't test this code right now, sorry if it's not working perfectly):
Project.find({ where: {id: 1}, include: [{model: Task, as: 'task'}]})
.then(function(project) {
project.task[0].updateAttributes({
status = 'done';
}).then(function() {
//done!
});
});
This answer may be helpful too.

Related

How to define many to many with Feathers and Sequelize with additional fields in join table?

I am struggling to find the solution for that.
I want to have users which can belong to many organizations.
Each user can have a different role (I would prefer even roles but it sounds even more complicated...) at a specific organization.
In the table like User_Organization_Role I need to have fields like role (roleId?), isActive. Maybe some more.
I am using Feathers Plus generator but I do not think it matters in this case, however it may be beneficial to add something to the schema file?
I thought having belongsTo with simple organizationId field will be sufficient but I've realized that changing that to manyToMany, later on, would be painful so I think it is much better to implement that now.
I will appreciate any solutions / suggestions / best practices etc.
n:m relations are by far the most difficult to handle, and there's really no one-size-fits-all solution. The biggest thing is to read and understand this page and its sub-pages, and then read them 2 more times for good measure. Try to focus on doing one thing at a time. I outline how I would approach this with feathersjs in this issue:
https://github.com/feathersjs/feathers/issues/852#issuecomment-406413342
The same technique could be applied in any application... the basic flow goes like this:
Create or update your primary objects first (users, organizations, roles, etc.). There are no relations made at this point. You need to have your objects created before you can make any relations.
Create or update the relations. This involves updating a "join" table (aka: "mapping" or "through" table) with data from step #1. The join table can (and should) have its own model. It should contain a foreign key for each of the objects you are associating (userId, organizationId, roleId etc.). You can put other fields in this table too.
Here is some pseudo code for how I would define my models (only showing relevant code for brevity). There is a little more to it than what I describe below, but this should get you started.
const UserOrganizationRole = sequelize.define('User_Organization_Role', {
// Define any custom fields you want
foo: DataTypes.STRING
})
// Let sequelize add the foreign key fields for you.
// Also, save a reference to the relationship - we will use it later
User.Organization = User.belongsToMany(Organization, { through: UserOrganizationRole });
User.Role = User.belongsToMany(Role, { through: UserOrganizationRole });
Organization.User = Organization.belongsToMany(User, { through: UserOrganizationRole });
Role.User = Role.belongsToMany(User, { through: UserOrganizationRole });
... and here is how I would go about handling inserts
const user = await User.create({ ... });
const org = await Organization.create({ ... });
const role = await Role.create({ ... });
await UserOrganizationRole.create({
userId: user.id,
organizationId: org.id,
roleId: role.id,
foo: 'bar'
});
... and finally, load the data like so:
// Now we can reference those relationships we created earlier:
const user = await User.findById(123, {
include: [User.Organization, User.Role]
});
const org = await Organization.findById(456, {
include: [Organization.User]
});

MongoDB - Populate document field recursively

I've got a Page:
const PageSchema = new Schema({
children: [{type: mongoose.Schema.Types.ObjectId, ref: 'Page'}]
});
As you can see each Page has an array of children which is also a Page.
Now what I need to do is fetch all "main" pages and populate their children array, easy enough except for the fact that I need to do this recursively since a Page contains an array of Pages.
MongoDB doesn't have any out of the box support for this, it only supports a 2 level deep population.
Here's my current query (removed all extra stuff for readability) without using the current .populate method (since it's not gonna work anyway):
Page.find(query)
.exec((err, pages) => {
if (err) {
return next(err);
}
res.json(pages);
});
I looked at this question which is similar but not exactly what I need:
mongoose recursive populate
That seems to use a parent to populate recursively and it also starts from just 1 document, rather than my scenario which uses an array of documents since I'm using .find and not .findOne for example.
How can I create my own deep recursive populate function for this?
Sidenote:
I am aware that the solution I need isn't recommended due to performance but I've come to the conclusion that it is the only solution that is going to work for me. I need to do recursive fetching regardless if it's in the frontend or backend, and doing it right in the backend will simplify things massively. Also the number of pages won't be big enough to cause performance issues.
You can recursively populate a field also like:
User.findOne({ name: 'Joe' })
.populate({
path: 'blogPosts',
populate: {
path: 'comments',
model: 'comment',
populate: {
path: 'user',
model: 'user'
}
}
})
.then((user) => {});
Please note that for first population, you don't need to specify model attribute, as it is already defined in your model's schema, but for next nested populations, you need to do that.
The answer actually lied in one of the answers from the previous questions, although a bit vague. Here's what I ended up with and it works really well:
Page.find(query)
.or({label: new RegExp(config.query, 'i')})
.sort(config.sortBy)
.limit(config.limit)
.skip(config.offset)
.exec((err, pages) => {
if (err) {
return next(err);
}
// takes a collection and a document id and returns this document fully nested with its children
const populateChildren = (coll, id) => {
return coll.findOne({_id: id})
.then((page) => {
if (!page.children || !page.children.length) {
return page;
}
return Promise.all(page.children.map(childId => populateChildren(coll, childId)))
.then(children => Object.assign(page, {children}))
});
}
Promise.all(pages.map((page) => {
return populateChildren(Page, page._id);
})).then((pages) => {
res.json({
error: null,
data: pages,
total: total,
results: pages.length
});
});
});
The function itself should be refactored into a utils function that can be used anywhere and also it should be a bit more general so it can be used for other deep populations as well.
I hope this helps someone else in the future :)

Delay response until all queries finished

My db contains projects and phases. Projects can have multiple phases. The models are similar to these:
Phase:
var phaseSchema = new mongoose.Schema({
project: { type: mongoose.Schema.Types.ObjectId, ref: 'Project' }
});
Project:
var projectSchema = new mongoose.Schema({
name : { type: String }
});
Currently I'm using the following approach to retrieve the phases for each project:
var calls = [];
var projects = _.each(projects, function (p) {
calls.push(function (callback) {
req.app.db.models.Phase.find({ project: p._id }, function (err, doc) {
if (err) {
callback(err);
} else {
p.phases = doc;
callback();
}
});
})
});
async.parallel(calls, function (err) {
workflow.outcome.projects = projects;
return workflow.emit('response');
});
As you can see I'm not passing anything to callback() just (ab)using async's parallel to wait with the response until the lookup finishes.
Alternatively I could pass the phase object to the callback but then in parallel I should iterate over phase and over projects to find the appropriate project for the current phase.
Am I falling into a common pitfall with this design and for some reason it would be better to iterate over the projects and the phases again, or I should take a completely different approach?
I actually think in this case you would be better of running one query to match all the potential results. For the "test" query you would issue all the _id values as an $in clause, then just do some matching on the results to your source array to assign the match(ed) document(s):
Matching all at once
// Make a hash from the source for ease of matching
var pHash = {};
_.each(projects,function(p) {
pHash[p._id.toString()] = p;
});
// Run the find with $in
req.app.db.models.Phase.find({ "project": { "$in": _.keys(pHash) } },function(err,response) {
_.each(response,function(r) {
// Assign phases array if not already there
if (!phash[r.project.toString()].hasOwnProperty("phases")
pHash[r.project.toString()].phases = [];
// Append to array of phases
pHash[r.project.toString()].phases.push(r)
});
// Now return the altered hash as orginal array
projects = _.mapObject(pHash,function(val,key) {
return val;
});
});
Also adding like you say "projects can have multiple phases", so the logic would be an "array" rather than an assignment of a single value.
More efficient $lookup
On the other hand, if you have MongoDB 3.2 available, then the $lookup aggregation pipeline operator seems to be for you. In this case you would just be working with the Projects model, but doing the $lookup on the `"phases" collection. With "collection" being the operative term here, since it is a server side operation that therefore only knows about collections and not the application "models":
// BTW all models are permanently registered with mongoose
mongoose.model("Project").aggregate(
[
// Whatever your match conditions were for getting the project list
{ "$match": { .. } },
// This actually does the "join" (but really a "lookup")
{ "$lookup": {
"from": "phases",
"localField": "_id",
"foreignField": "project",
"as": "phases"
}}
],function(err,projects) {
// Now all projects have an array containing any matched phase
// or an empty array. Just like a "left join"
})
);
That would be the most efficient way to handle this since all the work is done on the server.
So what you seem to be asking here is basically the "reverse case" of .populate() where instead of holding the "phases" as references on the "project" object the reference to the project is instead listed in the "phase".
In that case, either form of "lookup" should be what you are looking for. Either where you emulate that join via the $in and "mapping" stage, or directly using the aggregation framework $lookup operator.
Either way, this reduces the server contact down to "one" operation, where as your current approach is going to create a lot of connections and each up a fair amount of resources. Also no need to "Wait for all responses". I'd wager that both were much faster as well.

In bookshelf.js, I don't understand how to retrieve attribute from targets in hasOne() relationship for my template

I have a task object, the belongs to a job object. It is a belongsTo() relationship, but I'm using a hasOne() relationship because I just couldn't figure out belongsTo().
My models are:
var job = Bookshelf.Model.extend({
tableName: 'jobs',
idAttribute: 'id'
});
var task = Bookshelf.Model.extend({
tableName: 'tasks',
idAttribute: 'id',
job: function() {
return this.hasOne(Job);
},
job_summary: function() {
return this.job().fetch().get('summary');
}
});
I've tried a few combinations of the .job_summary() function, but nothing seems to work. When I create a task object, I just want to be able to get the job summary of the related task, like:
var task_jobsummary = task.job_summary();
What am I missing?
EDIT: I actually have 2 hasOne() targets I need to get data for.
EDIT: I think my problem is not understanding promises. If I were doing this in PHP, I'd do something like:
$myvar = stdClass();
$myvar->somefield = $task->getSomefield();
$myvar->anotherfield = $task->getAnotherField();
$myvar->jobfield = $job->getSomeJobField();
// ... etc
I want to simple return values from a then() after a fetch().
EDIT: Here is my code from my router:
var route_task(req, res, next) {
res.locals.username = 'debug_username';
res.locals.userpic = '/img/tmpuserpic.png';
new Model.Task().fetch({id: req.params.id, withRelated: ['job']})
.then(function(task) {
task.related('job').fetch().then(function(job) {
res.locals.somefield = job.get('somefield');
});
})
.then(function() {
res.render('task');
})
}
The two vars before the Model are set, and my template sees them. However, the template doesn't see the res.locals.somefield setting.
You are a little bit confused, you need to have the belongsTo where you have the key and the hasOne where basically it is pointed.
For example a post has many (but is equal in concept) comments and a comment belongs to a post.
In the model you just need to say what relationship are, when you want to retrive it (maybe in the controller) you need to do something like this
new Model({id:4})
.fetchOne({withRelated: ['thing']})
.then(function(model) {
// retrive the relation
var things = model.things(); // you already had the query done
});
You need to elaborate this but with the concept in mind the documentation helps a lot

Sequelize many to many documentation

I am trying to create a many to many association between projects and tasks using node and sequelize. I cannot figure out how to create new 'Tasks' with variable names that can be accumulated as an array for the project.setTasks function as the documentation here gives as an example with ALOT of detail missing. http://sequelizejs.com/documentation#associations-many-to-many
here is the example they give:
Project.hasMany(Task)
Task.hasMany(Project)
Project.create()...
Task.create()...
Task.create()...
// save them... and then:
project.setTasks([task1, task2]).success(function() {
// saved!
})
I'm not sure what you mean by "variable names" exactly.
However, here's an example from the sequelize test suite, as copied below with my comments:
// Define the models
this.User = this.sequelize.define('User', { username: DataTypes.STRING });
this.Task = this.sequelize.define('Task', { title: DataTypes.STRING, active: DataTypes.BOOLEAN });
// Setup associations
this.User.hasMany(this.Task);
this.Task.hasMany(this.User);
// using promises, sync the database, then create a user and two tasks
return this.sequelize.sync({ force: true }).then(function() {
return Promise.all([
self.User.create({ username: 'John'}),
self.Task.create({ title: 'Get rich', active: true}),
self.Task.create({ title: 'Die trying', active: false})
]);
}).spread(function (john, task1, task2) {
// This line below is only relevant to the tests.
self.tasks = [task1, task2];
// Using the created (and saved) tasks and user, set the tasks and return the promise
return john.setTasks([task1, task2]);
});
Edit I agree that the sequelize documentation is still too light, but I've found it a good framework so far to use. I recommend when in doubt either reading the unit tests, or writing your own tests to confirm behaviours.
Edit Note you can also use .addTask and .removeTask instead of simply setting them all. See the docs

Resources