Sequelize many to many with extra columns - node.js

After some research I didn't find anything related to my problem. So the setting is an M:M relationship already working with sequelize (sqllite):
return User.find({ where: { _id: userId } }).then(user => {
logger.info(`UserController - found user`);
Notification.find({ where: { _id: notificationId } }).then(notification => {
if (associate) {
return user.addNotification([notification]);
} else {
return user.removeNotification([notification]);
}
})
})
The thing is that I have extra fields in the inter table(cityId, active) and I don't know how to update it when running "addNotification".
Thanks in advance

If you are using Sequelize version 4.x there is some changes in the API
Relationships add/set/create setters now set through attributes by passing them as options.through (previously second argument was used as through attributes, now its considered options with through being a sub option)
user.addNotification(notification, { through: {cityId: 1, active: true}});

In order to add data to pivot table you should pass data as second parameter of add function
user.addNotification(notification, {cityId: 1, active: true});

When the join table has additional attributes, these can be passed in the options object:
UserProject = sequelize.define('user_project', {
role: Sequelize.STRING
});
User.belongsToMany(Project, { through: UserProject });
Project.belongsToMany(User, { through: UserProject });
// through is required!
user.addProject(project, { through: { role: 'manager' }});
You can find more about this here: https://sequelize.org/master/class/lib/associations/belongs-to-many.js~BelongsToMany.html

Related

Node Sequelize: How to include same table into 2 columns

I have 2 table:
User
Follow
Table follow has 2 foreign keys:
follower_id
followee_id
They are both foreign key to user table
I have the following associations created:
//Users-Followers
this.follows.belongsTo(this.users, {
as: 'follower',
foreignKey: 'follower_id'
});
this.users.hasMany(this.follows, {
as: 'folower',
foreignKey: 'follower_id'
});
//Users-Followees
this.follows.belongsTo(this.users, {
as: 'folowee',
foreignKey: 'followee_id'
});
this.users.hasMany(this.follows, {
as: 'folowee',
foreignKey: 'followee_id'
});
Now I want to: get who is following who.
At the following is my query:
async function getFollowByFollower(follower_id) {
let follows = model.follows;
let users = model.users;
let follow = await follows.findAll({
include: [
{
model: users,
as: 'follower',
required: true,
where: {
id: follower_id
}
},
{
model: users,
as: 'followee',
required: true
}
]
}).catch(function(err) {
console.log('followsDAO.js getFollowByFollower error: ' + err);
});
return follow;
};
Here goes the error message:
SequelizeEagerLoadingError: users is associated to follows multiple times. To identify the correct association, you must use the 'as' keyword to specify the alias of the association you want to include.
As you can see, I have clearly indicated 'as' in both query and association.
Please help.
Thanks.
Best regards,
Never mind. My code logic is 100% correct. The problem is in the association statement I misspelled "follower" as "folower" and "followee" as "folowee".
After correcting the spelling the code is running.

How to add dynamic additional attributes into a junction table with ManyToMany association in Sequelize

I created a many-to-many association by sequelize in my koa app. But I had no idea on how to create additional attributes in the junction table. Thanks.
I referred to the official doc of sequelize but didn't find a solution. In brief:
"an order can have many items"
"an item can exist in many orders"
Then I created OrderItems as junction table.
But I have trouble in inserting value into the junction table
// definitions
const Item = sequelize.define('item', itemSchema);
const Order = sequelize.define('order', orderSchema);
// junction table
const OrderItems = sequelize.define('order_item', {
item_quantity: { type: Sequelize.INTEGER } // number of a certain item in a certain order.
});
// association
Item.belongsToMany(Order, { through: OrderItems, foreignKey: 'item_id' });
Order.belongsToMany(Item, { through: OrderItems, foreignKey: 'order_id' });
// insert value
const itemVals = [{ name: 'foo', price: 6 }, { name: 'bar', price: 7 }];
const orderVals = [
{
date: '2019-01-06',
items: [{ name: 'foo', item_quantity: 12 }]
},
{
date: '2019-01-07',
items: [{ name: 'foo', item_quantity: 14 }]
}
]
items = Item.bulkCreate(itemVals)
orders = Order.bulkCreate(orderVals)
//Questions here: create entries in junction table
for (let order of orders) {
const itemsInOrder = Item.findAll({
where: {
name: {
[Op.in]: order.items.map(item => item.name)
}
}
})
order.addItems(itemsInOrder, {
through: {
item_quantity: 'How to solve here?'
}
})
}
// my current manual solution:
// need to configure column names in junction table manually.
// Just like what we do in native SQL.
const junctionValList =[]
for (let orderVal of orderVals) {
orderVal.id = (await Order.findOne(/* get order id */)).dataValues.id
for (let itemVal of orderVal.items) {
itemVal.id = (await Item.findOne(/* get item id similarly */)).dataValues.id
const entyInJunctionTable = {
item_id: itemVal.id,
order_id: orderVal.id,
item_quantity: itemVal.item_quantity
}
junctionValList.push(entyInJunctionTable)
}
}
OrderItems.bulkCreate(junctionValList).then(/* */).catch(/* */)
In case that this script it's for seeding purpose you can do something like this:
/*
Create an array in which all promises will be stored.
We use it like this because async/await are not allowed inside of 'for', 'map' etc.
*/
const promises = orderVals.map((orderVal) => {
// 1. Create the order
return Order.create({ date: orderVal.date, /* + other properties */ }).then((order) => {
// 2. For each item mentioned in 'orderVal.items'...
return orderVal.items.map((orderedItem) => {
// ...get the DB instance
return Item.findOne({ where: { name: orderedItem.name } }).then((item) => {
// 3. Associate it with current order
return order.addItem(item.id, { through: { item_quantity: orderedItem.item_quantity } });
});
});
});
});
await Promise.all(promises);
But it's not an efficient way to do it in general. First of all, there are a lot of nested functions. But the biggest problem is that you associate items with the orders, based on their name and it's possible that in the future you will have multiple items with the same name.
You should try to use an item id, this way you will be sure about the outcome and also the script it will be much shorter.

How do I reference an association when creating a row in sequelize without assuming the foreign key column name?

I have the following code:
#!/usr/bin/env node
'use strict';
var Sequelize = require('sequelize');
var sequelize = new Sequelize('sqlite:file.sqlite');
var User = sequelize.define('User', { email: Sequelize.STRING});
var Thing = sequelize.define('Thing', { name: Sequelize.STRING});
Thing.belongsTo(User);
sequelize.sync({force: true}).then(function () {
return User.create({email: 'asdf#example.org'});
}).then(function (user) {
return Thing.create({
name: 'A thing',
User: user
}, {
include: [User]
});
}).then(function (thing) {
return Thing.findOne({where: {id: thing.id}, include: [User]});
}).then(function (thing) {
console.log(JSON.stringify(thing));
});
I get the following output:
ohnobinki#gibby ~/public_html/turbocase1 $ ./sqltest.js
Executing (default): INSERT INTO `Users` (`id`,`email`,`updatedAt`,`createdAt`) VALUES (NULL,'asdf#example.org','2015-12-03 06:11:36.904 +00:00','2015-12-03 06:11:36.904 +00:00');
Executing (default): INSERT INTO `Users` (`id`,`email`,`createdAt`,`updatedAt`) VALUES (1,'asdf#example.org','2015-12-03 06:11:36.904 +00:00','2015-12-03 06:11:37.022 +00:00');
Unhandled rejection SequelizeUniqueConstraintError: Validation error
at Query.formatError (/home/ohnobinki/public_html/turbocase1/node_modules/sequelize/lib/dialects/sqlite/query.js:231:14)
at Statement.<anonymous> (/home/ohnobinki/public_html/turbocase1/node_modules/sequelize/lib/dialects/sqlite/query.js:47:29)
at Statement.replacement (/home/ohnobinki/public_html/turbocase1/node_modules/sqlite3/lib/trace.js:20:31)
It seems that specifying {include: [User]} instructs Sequelize to create a new User instance matching the contents of user. That is not my goal. In fact, I find it hard to believe that such behaviour would ever be useful—I at least have no use for it. I want to be able to have a long-living User record in the database and at arbitrary times create new Things which refer to the User. In my shown example, I wait for the User to be created, but in actual code it would likely have been freshly loaded through User.findOne().
I have seen other questions and answers say that I have to explicitly specify the implicitly-created UserId column in my Thing.create() call. When Sequelize provides an API like Thing.belongsTo(User), I shouldn’t have to be aware of the fact that a Thing.UserId field is created. So what is the clean API-respecting way of creating a new Thing which refers to a particular User without having to guess the name of the UserId field? When I load a Thing and specify {include: [User]}, I access the loaded user through the thing.User property. I don’t think I’m supposed to know about or try to access a thing.UserId field. In my Thing.belongsTo(User) call, I never specify UserId, I just treat that like an implementation detail I shouldn’t care about. How can I continue to avoid caring about that implementation detail when creating a Thing?
The Thing.create() call that works but looks wrong to me:
Thing.create({
name: 'A thing',
UserId: user.id
});
Option 1 - risks DB inconsistency
Sequelize dynamically generates methods for setting associations on instances, e.g. thing.setUser(user);. In your use case:
sequelize.sync({force: true})
.then(function () {
return Promise.all([
User.create({email: 'asdf#example.org'}),
Thing.create({name: 'A thing'})
]);
})
.spread(function(user, thing) {
return thing.setUser(user);
})
.then(function(thing) {
console.log(JSON.stringify(thing));
});
Option 2 - does not work/buggy
It isn't documented, but from a code dive I think the following should work. It doesn't but that seems to be because of a couple of bugs:
// ...
.then(function () {
return models.User.create({email: 'asdf#example.org'});
})
.then(function(user) {
// Fails with SequelizeUniqueConstraintError - the User instance inherits isNewRecord from the Thing instance, but it has already been saved
return models.Thing.create({
name: 'thingthing',
User: user
}, {
include: [{
model: models.User
}],
fields: ['name'] // seems nec to specify all non-included fields because of line 277 in instance.js - another bug?
});
})
Replacing models.User.create with models.User.build doesn't work because the built but not saved instance's primary key is null. Instance#_setInclude ignores the instance if its primary key is null.
Option 3
Wrapping the Thing's create in a transaction prevents an inconsistent state.
sq.sync({ force: true })
.then(models.User.create.bind(models.User, { email: 'asdf#example.org' }))
.then(function(user) {
return sq.transaction(function(tr) {
return models.Thing.create({name: 'A thing'})
.then(function(thing) { return thing.setUser(user); });
});
})
.then(print_result.bind(null, 'Thing with User...'))
.catch(swallow_rejected_promise.bind(null, 'main promise chain'))
.finally(function() {
return sq.close();
});
I have uploaded a script demo'ing option 2 and option 3 here
Tested on sequelize#6.5.1 sqlite3#5.0.2 I can use User.associations.Comments.foreignKey as in:
const Comment = sequelize.define('Comment', {
body: { type: DataTypes.STRING },
});
const User = sequelize.define('User', {
name: { type: DataTypes.STRING },
});
User.hasMany(Comment)
Comment.belongsTo(User)
console.dir(User);
await sequelize.sync({force: true});
const u0 = await User.create({name: 'u0'})
const u1 = await User.create({name: 'u1'})
await Comment.create({body: 'u0c0', [User.associations.Comments.foreignKey]: u0.id});
The association is also returned during creation, so you could also:
const Comments = User.hasMany(Comment)
await Comment.create({body: 'u0c0', [Comments.foreignKey]: u0.id});
and on many-to-many through tables you get foreignKey and otherKey for the second foreign key.
User.associations.Comments.foreignKey contains the foreignKey UserId.
Or analogously with aliases:
User.hasMany(Post, {as: 'authoredPosts', foreignKey: 'authorId'});
Post.belongsTo(User, {as: 'author', foreignKey: 'authorId'});
User.hasMany(Post, {as: 'reviewedPosts', foreignKey: 'reviewerId'});
Post.belongsTo(User, {as: 'reviewer', foreignKey: 'reviewerId'});
await sequelize.sync({force: true});
// Create data.
const users = await User.bulkCreate([
{name: 'user0'},
{name: 'user1'},
])
const posts = await Post.bulkCreate([
{body: 'body00', authorId: users[0].id, reviewerId: users[0].id},
{body: 'body01', [User.associations.authoredPosts.foreignKey]: users[0].id,
[User.associations.reviewedPosts.foreignKey]: users[1].id},
])
But that syntax is so long that I'm tempted to just hardcode the keys everywhere.

Sails:How to join two different models using waterline

In MVC peoples are using join query to join the two different tables, but In sails.js what I have to use? There is any method in waterline?
The answer based on database you are using.
For instance, you need to populate values in Mongo not to join. Or you need to join tables if you are using MySQL or similar.
In a nutshell, all this stuff is covered via Waterline. So you can just declare model in api/models with associations. Joining and populating is executing under the Waterline adapter.
For instance, you have User and Comment.
// api/models/User.js
module.exports = {
attributes: {
name: {
type: 'string'
},
comments: {
collection: 'Comment',
via: 'user'
}
}
};
// api/models/Comment.js
module.exports = {
attributes: {
text: {
type: 'string'
},
user: {
model: 'User',
via: 'comments'
}
}
};
Then you are execute User.find() and get already joined\populated tables from database.
But, if you want to execute manual joining, you can use .populate() method on Model instance. For instance:
// api/controllers/AnyController.js
module.exports = {
action: function(req, res) {
User
.findOne('ID_HERE')
.populate('comments')
.then(function(result) {})
.catch(function(error) {});
}
};
You can read more about populate here - http://sailsjs.org/documentation/reference/waterline-orm/queries/populate

Sequelize findall for every findall is possible?

I have this tables. Clients have Projects and Users works in Projects
Clients
- id
- name
Projects
- id
- name
- client_id
Users
- id
- name
UserProject
- user_id
- project_id
I try to return all users of the every project of client for example id=1
Finally result, something like this JSON:
[{
id:1
name:"Project1"
users:[{
id:23
name:"Robert Stark"
},{
id:67
name: "John Snow"
}]
}, {
id:2
name:"Project2"
users:[{
id:1
name:"Aria Stark"
}]
}]
If I find projects it works fine
req.tables.Project.findAll({
where: {
client_id:1
}
}).success(function(projects) {
...
If I find Users of a project it works fine
req.tables.UserProject.findAll({
where: {
project_id:1
},
include: [
{ model: req.tables.User, as: 'User' }
]
}).success(function(UsersProject) {
...
But, how can I combine both finAlls to return all users in every project? Something like the next code, but that works well. How can I do it?
I found this: Node.js multiple Sequelize raw sql query sub queries but It doesn't work for me or I do not know how to use it, because I have 2 loops not only one. I have projects loop and users loop
req.tables.Project.findAll({
where: {
client_id:1
}
}).success(function(projects) {
var ret_projects=[];
projects.forEach(function (project) {
var ret_project={
id:project.id,
name:project.name,
data:project.created,
users:[]
});
req.tables.UserProject.findAll({
where: {
project_id:project.id
},
include: [
{ model: req.tables.User, as: 'User' }
]
}).success(function(UsersProject) {
var ret_users=[];
UsersProject.forEach(function (UserProject) {
ret_users.push({
id:UserProject.user.id,
name:UserProject.user.name,
email:UserProject.user.email
});
});
ret_project.users=ret_users;
ret_project.push(ret_project)
});
});
res.json(projects);
});
Sounds like you already have a solution, but I came across the same issue and came up with this solution.
Very similar to what cvng said, just using nested include. So use:
Project.belongsTo(Client);
Project.hasMany(User);
User.hasMany(Project);
Then:
req.tables.Client.find({
where: { id:req.params.id },
include: [{model: req.tables.Project, include : [req.tables.User]}]
}).success(function(clientProjectUsers) {
// Do something with clientProjectUsers.
// Which has the client, its projects, and its users.
});
}
The ability to 'Load further nested related models' is described through the param 'option.include[].include' here: Sequelize API Reference Model.
Maybe this will be useful to someone else in the future.
Cheers!
I think you would not have to query UserProject entity directly but instead use Sequelize Eager loading methods to retrieve your entities.
Your models associations should look something like this :
Project.belongsTo(Client);
Project.hasMany(User, { as: 'Workers' });
User.hasMany(Project);
and once you have all projects related to client, your finder method :
Project
.findAll({ include: [{ model: User, as: 'Workers' })
.success(function(users) {
// do success things here
}
Take a look at, http://sequelizejs.com/docs/1.7.8/models#eager-loading.
Hope it helps !
Finally!!
cvng, your example helpme a lot, thanks.
But I have 3 levels Client, Project, thi is my final solution, is this a good solution?
req.tables.Client.find({
where: { id:req.params.id },
include: [{ model: req.tables.Project, as: 'Projects' }]
}).success(function(client) {
var ret ={
id:client.id,
name:client.name,
projects:[]
};
done = _.after(client.projects.length, function () {
res.json(ret);
});
client.projects.forEach(function (project) {
project.getUsers().success(function(users) {
var u=[]
users.forEach(function (user) {
u.push({
id:user.id,
name:user.name,
});
});
ret.projects.push({
id:project.id,
name:project.name,
users:u
});
done();
});
});
});

Resources