sequelize saving multiple objects that assosiates - javascript

I wasnt quite sure what to call this question but here is my setup:
var AcademyModule = sequelize.define('academy_module', {
academy_id: {
type: DataTypes.INTEGER,
primaryKey: true
},
module_id: {
type: DataTypes.INTEGER,
primaryKey: true
},
module_module_type_id: DataTypes.INTEGER,
sort_number: DataTypes.INTEGER,
requirements_id: DataTypes.INTEGER
}, {freezeTableName: true,}
With the following assosiation:
Requirements = sequelize.define('requirements', {
id: DataTypes.INTEGER,
value: DataTypes.STRING,
requirement_type_id: DataTypes.INTEGER
}, {freezeTableName: true});
AcademyModule.belongsTo(Requirements, {foreignKey: 'requirements_id'});
Now as you can see from my table setup i would have to save a row in requirements table and then use the inserted id to insert into the academy_module table.
for this i created the following:
add: function (requirements,academyModule,onSuccess, onError) {
var academyModule = academyModule;
if(requirements == null) {
AcademyModule.build(this.dataValues)
.save().ok(onSuccess).error(onError);
} else {
if(requirements.requirement_type_id == '1') {
requirements.value = requirements.module.id;
}
Requirements.create(requirements).then(function(createdReq) {
var associationPromises = [];
associationPromises.push(AcademyModule.create(this.dataValues));
return sequelize.Promise.all(associationPromises);
}).success(onSuccess).error(onError);
}
}
However in the then function i am unable to reach the academyModule object that contains the values that needs to be inserted.
This is a repeating problem for me and i really want to know how it is possible to connect so they do it automatically without doing small hacks
i have scouted the documentations but i havnt been able to find a single example of the above (which i find rather odd seeing as this is a fairly normal situation)
Jan's method
I tried to solve it using Jan's elegant method
However i am getting an error saying:
academy_module is not associated to requirements!
My code looks like this as for now:
var academyModule = academyModule;
if(requirements == null)
{
AcademyModule.build(this.dataValues)
.save().ok(onSuccess).error(onError);
}
else
{
requirements.academy_module = academyModule;
Requirements.create(requirements, {
include: [AcademyModule]
});
}
Funny thing here is t hat i have the assosiation:
AcademyModule.belongsTo(Requirements, {foreignKey: 'requirements_id'});
Requirements.hasOne(AcademyModule, {foreignKey: 'requirements_id'});

First of all, I cannot answer why you are having problems accessing academyModule - it should be available via simple javascript scoping.
There are some unexplained bits of your code... You are pushing a single promise to associationPromises - Why not just return that promise?
You are accessing this.dataValues to create an AcademyModule instance - this will normally be undefined inside a promise handler.
Your issue could be solved more elegantly with nested creation, which is in 2.0.5: https://github.com/sequelize/sequelize/pull/3386
First of all you need to create the reverse association from Requirements --> AcademyModule
Requirements.hasOne(AcademyModule, {foreignKey: 'requirements_id'});
This is needed because the Requirement is the 'main model', which is used to create the AcademyModule
requirements.academy_module = academy_module;
Requirements.create(requirements, {
include: [AcademyModule]
});
By setting academy_module on the requirements object, and adding include as the 2nd parameter (similar to how you use includes for find) both are created and associated in one go.
You'll need to modify your models slightly. With your current model definition the table will contain an id column, but sequelzie does not know that it is the primary key. You either need to remove id completely from the Requirements model (in which case sequelize will add it automatically and mark it as primary key), or add primaryKey to it:
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true // optional - only if you actually want AI :)
}

Related

Sequelize: how to do a WHERE condition on joined table with left outer join

My database model is as follows:
An employee drives one or zero vehicles
A vehicle can be driven by one or more employees
A vehicle has a model type that tells us it's fuel type amongst other things.
I'd like sequelize to fetch me all employees where they don't drive a vehicle, or if they do then the vehicle is not diesel.
So where VehicleID is null OR Vehicle.VehicleModel.IsDiesel = false
My current code is as follows:
var employee = sequelize.define('employee', {
ID: Sequelize.INTEGER,
VehicleID: Sequelize.INTEGER
});
var vehicle = sequelize.define('vehicle', {
ID: Sequelize.INTEGER,
ModelID: Sequelize.INTEGER
});
var vehicleModel = sequelize.define('vehicleModel', {
ID: Sequelize.INTEGER,
IsDiesel: Sequelize.BOOLEAN
});
employee.belongsTo(vehicle);
vehicle.belongsTo(vehicleModel);
If I run the following:
options.include = [{
model: model.Vehicle,
attributes: ['ID', 'ModelID'],
include: [
{
model: model.VehicleModel,
attributes: ['ID', 'IsDiesel']
}]
}];
employee
.findAll(options)
.success(function(results) {
// do stuff
});
Sequelize does a left outer join to get me the included tables. So I get employees who drive vehicles and who don't.
As soon as I add a where to my options:
options.include = [{
model: model.Vehicle,
attributes: ['ID', 'ModelID'],
include: [
{
model: model.VehicleModel,
attributes: ['ID', 'IsDiesel']
where: {
IsDiesel: false
}
}]
}];
Sequelize now does an inner join to get the included tables.
This means that I only get employees who drive a vehicle and the vehicle is not diesel. The employees who don't drive a vehicle are excluded.
Fundamentally, I need a way of telling Sequelize to do a left outer join and at the same time have a where condition that states the column from the joined table is false or null.
EDIT:
It turns out that the solution was to use required: false, as below:
options.include = [{
model: model.Vehicle,
attributes: ['ID', 'ModelID'],
include: [
{
model: model.VehicleModel,
attributes: ['ID', 'IsDiesel']
where: {
IsDiesel: false
},
required: false
}],
required: false
}];
I had already tried putting the first 'required:false' but I missed out on putting the inner one. I thought it wasn't working so I gave up on that approach. Dajalmar Gutierrez's answer made me realise I needed both for it to work.
When you add a where clause, sequelize automatically adds a required: true clause to your code.
Adding required: false to your include segment should solve the problem
Note: you should check this issue iss4019
Eager loading
When you are retrieving data from the database there is a fair chance that you also want to get associations with the same query - this is called eager loading. The basic idea behind that, is the use of the attribute include when you are calling find or findAll.
when you set
required: false
will do
LEFT OUTER JOIN
when
required: true
will do
INNER JOIN
for more detail docs.sequelizejs eager-loading

Sequelize Migration: update model after updating column attributes

I will get through to the point already. I'm having a problem of updating the rows after I have changed the status column attribute.
up: function(queryInterface, Sequelize) {
return queryInterface.changeColumn('projects', 'status', {
type: Sequelize.ENUM('processing', 'unassigned', 'ongoing', 'completed'),
allowNull: false,
defaultValue: 'unassigned'
}).then(function() {
return Project.update({
status: 'unassigned'
}, {
where: {
status: 'processing'
}
});
});
}
The Project.update() seems not working in any case but changing the attributes of the column works.
Any idea guys? I'm somehow a newbie in sequelize and any idea would be a great help. Thanks.
Depending on how you execute the migration ( via sequelize-cli or programmatically via umzug ). There is a different way to expose the table via the ORM.
In your case you have queryInterface passed as an argument to your function. So you can do a "raw query" via the attached sequelize property.
up: function(queryInterface, Sequelize) {
return queryInterface.changeColumn('projects', 'status', {
type: Sequelize.ENUM('processing', 'unassigned', 'ongoing', 'completed'),
allowNull: false,
defaultValue: 'unassigned'
}).then(function() {
return queryInterface.sequelize
.query("UPDATE projects SET status='unassigned' WHERE status='processing'");
});
}
By doing this you will make a raw Query to your database.
You can check out this gist for more details on an advanced way of using the ORM inside the migration.
I'm a fan of using umzug programmatically, which executes the migrations and also provides the initialized models of your database. If you configure it properly, you will benefit the exposed models ( e.g. sequelize.model('project').update() ) and have a better looking code.

Ignore default model attributes in SailsJS for 1 specific model

Hi everyone im stuck at using a model of a specific table of a mysql database. I am using 2 different databases in my SailsJS application. One of the two databases has been created before the SailsJS application, so the tables in this database doesn't have the default attributes configured in config/models.js.
This causes an error when I try to call for example the find() function on the model that uses the older database because it misses a column. See following error:
: ER_BAD_FIELD_ERROR: Unknown column 'tbl_user.deleted' in 'field list'
I don't want to add the default attributes to the older database columns, so is it possible to ignore the default attributes configured in config/models.js for specific models?
After trying a few things i came up with the following solution.
Just add the default attributes to your model but add it as an function.
module.exports = {
connection: connection,
migrate: 'safe',
tableName: 'tbl_name',
autoCreatedAt: false,
autoUpdatedAt: false,
autoPK: false,
schema: true,
attributes: {
id: {
columnName: 'id',
type: 'string',
primaryKey: true
},
name: {
columnName: 'name',
type: 'string'
},
email: {
columnName: 'email',
type: 'string'
},
deleted: function(){
var obj = this.toObject();
delete obj.deleted;
return obj;
},
createdBy: function(){
var obj = this.toObject();
delete obj.createdBy;
return obj;
}
}
};
In this example the attributes deleted and createdBy are default attributes in config/models.js. I made a function of these attributes in the specific model. In this function i delete this attribute and return the object without the deleted attribute.

allow null value for sequelize foreignkey?

How do I allow null for a foreignkey? I have a through and belongsToMany association:
Shelf
belongsToMany(Book, { through: Library, as: "Books"});
Book
belongsToMany(Shelf, { through: Library, as: "Shelves"});
Library
section: DataTypes.STRING,
// ... other fields
I would like to record a null on the bookId in Library:
Library.create({
section: "blah",
bookId: null,
shelfId: 3
});
Since Sequelize automatically creates the bookId and shelfId in the join table Library, how would I specify that I want to allow a null on the bookId foreignkey in Library?
I am no expert on node.js nor sequelize.js, but with one search in google I got the official documentation. You might need to pay some attention to this part of the code in the documentation:
Library.hasMany(Picture, {
foreignKey: {
name: 'uid',
allowNull: false
}
});
It seems you need to specify the {foreignKey: {name: 'bookId', allowNull: true} } in Library.
By default, the Sequelize association is considered optional. If you would like to make the foreign key required, you can set allowNull to be false.
Foo.hasOne(Bar, {
foreignKey: {
allowNull: false
}
});
See the docs

Add multiple records to model's collection Sailsjs

I have the following models in my Sailsjs application with a many-to-many relationship:
event.js:
attributes: {
title : { type: 'string', required: true },
description : { type: 'string', required: true },
location : { type: 'string', required: true },
maxMembers : { type: 'integer', required: true },
currentMembers : { collection: 'user', via: 'eventsAttending', dominant: true },
creator : { model: 'user', required: true },
invitations : { collection: 'invitation', via: 'eventID' },
tags : { collection: 'tag', via: 'taggedEvents', dominant: true },
lat : { type: 'float' },
lon : { type: 'float' },
},
tags.js:
attributes: {
tagName : { type: 'string', unique: true, required: true },
taggedEvents : { collection: 'event', via: 'tags' },
},
Based on the documentation, this relationship looks correct. I have the following method in tag.js that accepts an array of tag strings, and an event id, and is supposed to add or remove the tags that were passed in:
modifyTags: function (tags, eventId) {
var tagRecords = [];
_.forEach(tags, function(tag) {
Tag.findOrCreate({tagName: tag}, {tagName: tag}, function (error, result) {
tagRecords.push({id: result.id})
})
})
Event.findOneById(eventId).populate('tags').exec(function(error, event){
console.log(event)
var currentTags = event.tags;
console.log(currentTags)
delete currentTags.add;
delete currentTags.remove;
if (currentTags.length > 0) {
currentTags = _.pluck(currentTags, 'id');
}
var modifiedTags = _.pluck(tagRecords, 'id');
var tagsToAdd = _.difference(modifiedTags, currentTags);
var tagsToRemove = _.difference(currentTags, modifiedTags);
console.log('current', currentTags)
console.log('remove', tagsToRemove)
console.log('add', tagsToAdd)
if (tagsToAdd.length > 0) {
_.forEach(tagsToAdd, function (tag) {
event.tags.add(tag);
})
event.save(console.log)
}
if (tagsToRemove.length > 0) {
_.forEach(tagsToRemove, function (tagId) {
event.tags.remove(tagId)
})
event.save()
}
})
}
This is how the method is called from the event model:
afterCreate: function(record, next) {
Tag.modifyTags(tags, record.id)
next();
}
When I post to event/create, I get this result: http://pastebin.com/PMiqBbfR.
It looks as if the method call itself is looped over, rather than just the tagsToAdd or tagsToRemove array. Whats more confusing is that at the end, in the last log of the event, it looks like the event has the correct tags. When I then post to event/1, however, the tags array is empty. I've also tried saving immediately after each .add(), but still get similar results.
Ideally, I'd like to loop over both the tagsToAdd and tagsToRemove arrays, modify their ids in the model's collection, and then call .save() once on the model.
I have spent a ton of time trying to debug this, so any help would be greatly appreciated!
There are a few problems with your implementation, but the main issue is that you're treating certain methods--namely .save() and .findOrCreate as synchronous methods, when they are (like all Waterline methods) asynchronous, requiring a callback. So you're effectively running a bunch of code in parallel and not waiting for it to finish before returning.
Also, since it seems like what you're trying to do is replace the current event tags with this new list, the method you came up with is a bit over-engineered--you don't need to use event.tags.add and event.tags.remove. You can just use plain old update.
So you could probably rewrite the modifyTags method as:
modifyTags: function (tags, eventId, mainCb) {
// Asynchronously transform the `tags` array into an array of Tag records
async.map(tags, function(tag, cb) {
// For each tag, find or create a new record.
// Since the async.map `cb` argument expects a function with
// the standard (error, result) node signature, this will add
// the new (or existing) Tag instance to the resulting array.
// If an error occurs, async.map will exit early and call the
// "done()" function below
Tag.findOrCreate({tagName: tag}, {tagName: tag}, cb);
}, function done (err, tagRecords) {
if (err) {return mainCb(err);}
// Update the event with the new tags
Event.update({id: eventId}, {tags: tagRecords}).exec(mainCb);
});
}
See the full docs for async.map here.
If you wanted to stick with your implementation using .add and .remove, you would still want to use async.map, and do the rest of your logic in the done method. You don't need two .save calls; just do run all the .add and .remove code first, then do a single .save(mainCb) to finish it off.
And I don't know what you're trying to accomplish by deleting the .add and .remove methods from currentTags (which is a direct reference to event.tags), but it won't work and will just cause confusion later!

Categories

Resources