Association sequelize 3 tables, many to many - node.js

I have 4 tables
- client
- project
- user
- userProject
One Project belongs to client and it needs to have client foreign key client_id.
UserProject has project_id and user_id foreign keys, belongs to project and user.
One user owns the clients of his projects.
How can I list the clients of one user?

I'm wondering you could use eager loading feature from sequalize:
Client.findAll({
include: [{
model: Project,
include: [{
model: User,
where: {
id: <UserId>
},
required: false
}]
}]
}).then(function(clients) {
/* ... */
});

This creates a many-to-many relation between user and project. Between user and client you therefor have a many-to-many-to-one relation. This is not supported by sequelize. I would have created an instance method on the User model like this:
User = sequelize.define('user', {
// ... definition of user ...
},{
instanceMethods: {
getClients: function() {
return this.getProjects().then(function (projects) {
var id_map = {};
for (var i = 0; i < projects.length; i++) {
id_map[projects[i].clientId] = 1;
}
return Client.findAll({
where: { id: [Object.keys(id_map)] }
});
});
}
}
});
and then call this function when you have a user instance like this, to get the clients:
user.getClients().then(function (clients) {
console.log(clients);
});

Related

How to query in sequelize by an association without loading the associated object?

I have multiple models which are associated to each other.
e.g:
var User = sequelize.define("user")
var Project = sequelize.define("project")
Project.hasMany(User)
Now I want to query all Projects containing a specific user.
e.g.:
Project.findAll({
include: [
{
model: User,
where: { id }
}
]
})
This works, but loads also the user and attaches it to the project.
How can I tell sequelize, that the user should not be added to the found projects?
I just managed to address the same problem (using Sequelize 4).
You can specify that you don't want any fields of User just using attributes: [], so your code would become:
Project.findAll({
include: [
{
attributes: [],
model: User,
where: { id }
}
]
})
Many-to-many relationships are defined using the belongsToMany() method in sequelize on both sides. For your specific use case you would have to use a through model for the relation and query the through model directly.
var User = sequelize.define("user")
var Project = sequelize.define("project")
var ProjectUser = sequelize.define("projectUser")
Project.belongsToMany(User, {
through: ProjectUser
})
User.belongsToMany(Project, {
through ProjectUser
});
ProjectUser.findAll({
where: {
UserId: 'someId'
},
// We only want the project, not the user.
// You might need to do ProjectUser.belongsTo() for both
// models for this to work.
include: [Project]
})
.then(function(results) {
// Here we are getting an array of ProjectUsers, to return all the projects
// we map it to a new array of only projects.
return results.map(function(userProject) {
return userProject.Project;
});
})

Should one-to-one associations populate both sides?

I have two models (user & agent). I then create a user and an agent. I am expecting to see the association when using the blueprint routes for BOTH /user and /agent. I am only seeing the user model is associated with an agent via the /agent blueprint. The /user blueprint does not have any reference/association to an Agent.
The issue presents itself when I am trying to access the Agent via A userId with the following command:
User.findOne(req.body.userId).populate('agent').exec(function(err, agent)
"agent" is in fact the user information...not the agent.
Here are my models:
User:
attributes: {
agent: {
model: 'agent',
via: 'owner'
}
}
Agent:
attributes: {
owner: {
model: 'user'
}
}
Thanks for reading!
Sails does not fully support one-to-one model associations--you have to set the "foreign key" on both sides. See https://stackoverflow.com/a/27752329/345484 for more information. I'm tempted to just close this question as a duplicate of that one, but the setup is slightly different.
You have understood the populate wrong. populate() doesn't replace last call with new information. It takes attribute from model (that you specify in populate('attribute')) and replace id of that attribute in your model with another model information looked up by its id.
Let's dive into example.
You have User and Agent model.
// api/models/User.js
module.exports = {
attributes: {
agent: {
model: 'Agent',
via: 'owner'
}
}
};
// api/models/Agent.js
module.exports = {
attributes: {
owner: {
model: 'User',
via: 'agent'
}
}
};
You are calling User.findOne(userId).populate('agent').exec(function(err, user) {} and expect to get only agent as I understood. Wrong. It returns User model with Agent model as an attributes of User model.
// api/controllers/AnyController.js
module.exports = {
index: function(req, res) {
User
.findOne(req.param('userId'))
.populate('agent')
.then(function(user) {
console.log(user); // User information
console.log(user.agent); // Agent information for that user
return user;
})
.then(res.ok)
.catch(res.negotiate);
}
};
You can read about population more here - http://mongoosejs.com/docs/populate.html

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

Creating instance with an association in Sequelize

Using Sequelize, I've created two models: User and Login.
Users can have more than one Login, but a login must have exactly one user, which means a Login cannot be saved without a User ID.
How do I .create a Login with a User association all in one swoop?
Current Code (Doesn't Work)
// Set up the models
var User = sequelize.define('User', {});
var Login = sequelize.define('Login', {});
Login.belongsTo(User, {
onDelete: 'cascade',
foreignKey: {
field: 'userId',
allowNull: false,
}
});
// Create the instances
var user = User.create().then(function() {
// THIS IS WHERE I WOULD LIKE TO SET THE ASSOCIATION
var login = Login.create({
userId: user.get('id')
});
)};
The above results in SequelizeValidationError: notNull Violation: UserId cannot be null
Assuming you have the right association between users and login,
you can just create a user including a login:
User.create({
name: "name",
Login: {...}
},{
include: Login
})
you can find more information here:
https://sequelize.org/docs/v6/advanced-association-concepts/creating-with-associations/
First of all you need to setup the relations in both ways, like this:
// Set up the models
var User = sequelize.define('User', {});
var Login = sequelize.define('Login', {});
// Set the correct associations
User.hasMany(Login, {})
Login.belongsTo(User, {});
Then, you need to properly get the instances returned by the promises:
// Create the instances
User.create({}).then(function(newUser) {
// now you can use newUser acessors to create the login
return newUser.createLogin({});
).then(function(newLogin){
// newLogin
}).catch(function(error){
// error
});
In your .then, the callback receives the model instance created by the previous call. You need to specify the argument inside the callback function.
var user = User.create().then(function(user) {
// THIS IS WHERE I WOULD LIKE TO SET THE ASSOCIATION
var login = Login.create({
userId: user.get('id')
});
return login
}).then(function(login) {
// all creation are complete. do something.
});
Also something important I would like to point out is your missing var statements! Those are important but not related to this question. See Declaring variables without var keyword
An Update to #Arwed Mett's answer
//Create Association Alias or just setting association alias by using 'as' keyword will also work
Login.User = Login.belongsTo(User);
User.create({
name: "name",
Login: {...}
}, {
include: [{
association: Login.User
}]
});
Refrence link - http://docs.sequelizejs.com/manual/tutorial/associations.html#creating-with-associations
For those like me who were trying to create an instance of a model including another instance, like:
var login1 = await Login.create(...);
var user1 = await User.create({
Login: login1
}, {
include: Login
});
You can't because this method is used to embed an instance (Login) which is not already existing and that will be created at the parent instance (User) creation level.
So, if you want to embed an already existing Login in the newly created User, do instead:
var login1 = await Login.create(...);
var user1 = await User.create({
loginId: login1.get('id')
}, {});
You have association between User an Login with constraint allowNull at false. You must create Login before User or set allowNull at true in model and the table to DB (LoginId Null constraint)
var User = sequelize.define('User', {});
var Login = sequelize.define('Login', {});
Login.belongsTo(User, {
onDelete: 'cascade',
foreignKey: {
field: 'userId',
allowNull: false,
}
});
Solution
Login.create({
username: "username",
User: {...}
},{
include: User
})
I have the same issue recently!
I have a typo mistake with the foreignKey config. Use field instead of name caused the issue.
The change below will fix it.
{
foreignKey: {
name: 'userId',
allowNull: false,
}
}
As an extra you could also nest your creation to be even more effective and concise.
// Set up the models
var User = sequelize.define('User', {});
var Login = sequelize.define('Login', {});
...
User.create({
name: "name",
Login:
{
users: {..i.e several users if a user belongs to another user..}
}
},{
include:{
model: Login,
include: User //nested model.Create
}
})
as seen here: https://github.com/sequelize/sequelize/issues/7252

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