How to generate query on join table using Sequelize? - sql

Let us say there are two tables namely User and User Role.
The relationship between user and user role is one to many.
Sequelize model for the user is as following -
const user = sequelize.define(
'user', {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
userName: {
type: DataTypes.STRING(200),
allowNull: false,
field: 'username'
},
password: {
type: DataTypes.STRING(200),
allowNull: false,
field: 'password'
}
}, {
tableName: 'user'
}
);
Sequelize model for user role is as follwing -
const userRole = sequelize.define(
'userRole', {
id: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
userId: {
type: DataTypes.BIGINT,
allowNull: false,
primaryKey: true,
autoIncrement: true,
field: 'user_id'
},
password: {
type: DataTypes.STRING(200),
allowNull: false,
field: 'password'
}
}, {
tableName: 'userRole'
}
);
Sequelize association is defined as follows -
user.hasMany(models.userRole, { foreignKey: 'user_id', as: 'roles' });
userRole.belongsTo(models.user, { foreignKey: 'user_id', as: 'user' });
I want to generate the following query using
Sequelize -
SELECT *
FROM USER
INNER JOIN (SELECT user_role.user_id,
role
FROM user_role
INNER JOIN USER tu
ON tu.id = user_role.user_id
GROUP BY user_id
ORDER BY role) AS roles
ON USER.id = roles.user_id;
I am developing an API which will be consumed by the front end grid for showing user info. There is search functionality on role attribute of user role table. If any of role of a specific user is matched then I expect a user record with all the roles which are associated with that user.

To get all roles that are associated with the user even if one of them matches with a query you need to define another association so that you can include role table twice in sequelize statement.
User.hasMany(models.UserRole, { foreignKey: 'user_id', as: 'roles2' });
Sequelize statement -
const userInfo = await User.findAndCountAll({
include: [
{
model: UserRole,
attributes: ['id', 'role'],
as: 'roles',
where: { [Op.or]: [{ role: { [Op.like]: '%MANAGER%' } },
required: true
},
{
model: UserRole,
attributes: ['id', 'role'],
as: 'roles2',
required: true
}
],
where: whereStatement,
});
With the help of first include (join), you can filter user records based on user role and with the help of the second include(join), you can get all the roles of a user whose one of the roles is matched.

You have to use include (regarding the doc : https://sequelize.org/master/manual/models-usage.html#-code-findandcountall--code----search-for-multiple-elements-in-the-database--returns-both-data-and-total-count)
Exemple :
Models.User.findAll({
where :{
id: userId
},
group: ['roles.user_id'],
order: [['roles.role', 'ASC']] //or DESC, as you want
include: {
model: Models.UserRole,
as: 'roles',
attributes: ['user_id', 'role'],
required: true
},
})
Hope it helps you

Related

Counting in Many-To-Many Relations Sequelize

I am trying to count number of Followers and Followings of a user.
as followingCount and followerCount
User Model
User.init(
{
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
},
email: {
type: DataTypes.STRING,
},
password: {
type: DataTypes.STRING,
},
}
);
static associate(models) {
// Follow relationship
this.belongsToMany(models.User, {
through: 'UserFollow',
foreignKey: 'followerId',
as: 'following',
});
// Follow relationship
this.belongsToMany(models.User, {
through: 'UserFollow',
foreignKey: 'followeeId',
as: 'follower',
});
}
Where UserFollow is a Joint Table with columns followeeId and followerId.
My current approach for finding number of followings is something like this :
const user = await User.findOne({
where: {
id,
},
attributes: [
'id',
'userName',
'email',
[sequelize.fn('COUNT', sequelize.col('following->UserFollow.followeeId')), 'followingCount'],
],
include: [
{
model: User,
as: 'following',
attributes: ['id', 'userName', 'email'],
through: {
attributes: [],
},
},
],
group: ['User.id', 'following.id'],
});
return user;
And Output getting is like this:
Here I am getting followingCount as 1... but it should be 3.
"data": {
"id": "1af4b9ea-7c58-486f-a37a-e46461487b06",
"userName": "xyz",
"email": "xyz#gmail.com",
"followingCount": "1", <------ I want this to be 3
"following": [
{
"id": "484202b0-a6d9-416d-a8e2-6681deffa3d1",
"userName": "uqwheuo",
"email": "uqwheuo#gmail.com"
},
{
"id": "56c8d9b0-f5c6-4b2e-b32c-be6363294614",
"userName": "aiwhroanc",
"email": "aiwhroanc#gmail.com"
},
{
"id": "9a3e4074-c7a0-414e-8df4-cf448fbaf5fe",
"userName": "iehaocja",
"email": "iehaocja#gmail.com"
}
]
}
I am not able to count in Joint Table..
The reason that you are getting followingCount: 1 is that you group by following.id (followeeId). It only counts unique followeeId which is always 1.
Although, if you take out following.id from group, the SQL doesn't work any more. It will crash with "a column must appear in GROUP BY clause...". This is a common issue in Postgres and this link (https://stackoverflow.com/a/19602031/2956135) explains the topic well in detail.
To solve your question, instead of using group, you can use COUNT OVER (PARTITION BY).
const user = await User.findOne({
where: {
id,
},
attributes: [
'id',
'userName',
'email',
[Sequelize.literal('COUNT("following->UserFollow"."followeeId") OVER (PARTITION BY "User"."id")'), 'followingCount']
],
include: [
{
model: User,
as: 'following',
attributes: ['id', 'userName', 'email'],
through: {
attributes: [],
}
},
],
});
======================================================
Update:
The original query only fetch "following" relationship. In order to fetch followers of this user, you first need to add "follower" association.
Then, since 2 associations is added, we need to add 1 more partition by column to count exactly the followers or followees.
const followeeIdCol = '"following->UserFollow"."followeeId"';
const followerIdCol = '"follower->UserFollow"."followerId"';
const user = await User.findOne({
where: {
id,
},
attributes: [
'id',
'userName',
'email',
// Note that the COUNT column and partition by column is reversed.
[Sequelize.literal(`COUNT(${followeeIdCol}) OVER (PARTITION BY "Users"."id", ${followerIdCol})`), 'followingCount'],
[Sequelize.literal(`COUNT(${followerIdCol}) OVER (PARTITION BY "Users"."id", ${followeeIdCol})`), 'followerCount'],
],
include: [
{
model: User,
as: 'following',
attributes: ['id', 'userName', 'email'],
through: {
attributes: [],
}
},
{
model: User,
as: 'follower', // Add follower association
attributes: ['id', 'userName', 'email'],
through: {
attributes: [],
}
},
],
});

define belongsToMany relation with self referencing table in sequelize

i have "User" table with phoneNumber as a table column.
I want to have association so that each User can add many contacts... basically contacts will be other User.
Usually in many to many relation we have two tables and a joining table.
But can i have one table, that is User table and a joining table referencing to User itself ??
my User table:
const User = sequelize.define(
"users",
phone_number: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
} );
can i have following "UserContact" joining table without "Contact table" with this association
User.associate = (models) => {
models.User.belongsToMany(models.Contact, {through: 'UserContact' })
};
my UserContact table:
const UserContact = sequelize.define(
"user_contacts",
{
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
contact_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
}
);
Is there any other way like referencing contacts through some field in User table itself?? again one user should have one or many contacts.
You can indicate User model in a association and indicate fields for both sides:
User.associate = (models) => {
models.User.belongsToMany(models.User, {through: 'UserContact' }, foreignKey: 'user_id', otherKey: 'contact_id')
};

Sequelize define association on Array

I have two models in Sequelize as below:
export const User = db.define('user', {
id: {
type: Sequelize.UUID,
primaryKey: true,
},
});
export const Class = db.define('class', {
id: {
type: Sequelize.UUID,
primaryKey: true,
},
students: Sequelize.ARRAY({
type: Sequelize.UUID,
references: { model: 'users', key: 'id' },
})
});
How can I define an association between my Class model and the user model?
I have tried the below but it gives me an error.
Class.hasMany(User, { foreignKey: 'students' });
User.belongsTo(Class);
DatabaseError [SequelizeDatabaseError]: column "class_id" does not exist
I think you are missing the syntax
students: Sequelize.ARRAY({
type: Sequelize.UUID,
references: { model: 'users', key: 'id' }, // this has no effect
})
Should be
students: {
type: DataTypes.ARRAY({ type: DataTypes.UUID }),
references: { model: 'users', key: 'id' }
}
This won't work either, because the data type of students (ARRAY) and id (UUID) of User does not match.
Also, with these associations, you are adding two columns on User referencing id of Class but you only need one
Class.hasMany(User, { foreignKey: 'students' }); //will add students attribute to User
User.belongsTo(Class); //will add classId attribute to User
if you want to name the foreign key column passe the same name to both associations, by default Sequelize will add classId, however if you configured underscored: true on the models it will be class_id
Here is a working solution
const User = sequelize.define('user', {
id: { type: DataTypes.UUID, primaryKey: true },
});
const Class = sequelize.define('class', {
id: { type: DataTypes.UUID, primaryKey: true },
students: DataTypes.ARRAY({ type: DataTypes.UUID }),
});
Class.hasMany(User, { foreignKey: 'class_id' });
User.belongsTo(Class, { foreignKey: 'class_id' });

Sequelize the nodejs throught model association

I have tree models User, Company and UserCompany.
I have associations
Company.belongsToMany(User, {
as: 'users',
through: {
model: UserCompany,
unique: false
},
onDelete: 'cascade'
});
User.belongsToMany(Company, {
as: 'companies',
through: {
model: UserCompany,
unique: false
},
onDelete: 'cascade'
});
I am trying to query companies buy runing this code
sql.Company.findAll({
where: query,
include: [{model:sql.User, as:'users', attributes: ['id', 'first_name', 'last_name', 'email']}],
order: sort,
limit: limit,
offset: offset
})
I have two elements of UserCompany with CompanyId=10 and UserId=50, but query returns only one of them.
It returns array
{id: 10,
...,
users: {
id: 50,
...,
UserCompany: {}
}
}
So UserCompany is not array, it is just one element. But I want to get them all.
How can I fix my association code?
Your associations are right. The UserCompany property isn't supposed to be showing all of each user's UserCompany relationships -- it's just showing the one that got the user included in that query's results.
If what you're looking for is a list of companies associated with each user in the returned array, you can add an include for that:
Company.findAll({
include: [{
model: User,
as: 'users',
attributes: ['id', 'first_name', 'last_name', 'email'],
// Add this
include: [{
model: Company,
as: 'companies',
attributes: ['id', 'name']
}],
}],
})
This gives you the following format in the returned array:
{
id: 1,
first_name: 'Nelson',
last_name: 'Bighetti',
email: 'bighead#hooli.com',
UserCompany: {...},
companies: [
{ id: 1, name: 'Hooli', UserCompany: {...} },
{ id: 2, name: 'Pied Piper', UserCompany: {...} }
]
}

Sails.js + Waterline: Many-to-Many through association

I'm new to Sails.js (v0.10.5) and Waterline ORM. I have 3 tables in database: users (id, name), roles(id, alias) and join table users_roles(user_id, role_id). It's important not to change table names and field names in database. I want Policy entity to be a join entity between User and Role. Here is some mapping code:
//User.js
module.exports = {
tableName: 'users',
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
id: {
type: 'integer',
required: true
},
name: {
type: 'string'
},
roles: {
collection: 'role',
via: 'users',
through: 'policy'
},
}
}
//Role.js
module.exports = {
tableName: "roles",
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
id: {
type: 'integer',
required: true
},
alias: {
type: 'string',
required: true
},
users: {
collection: 'user',
via: 'roles',
through: 'policy'
}
}
}
//Policy.js
module.exports = {
tableName: "users_roles",
tables: ['users', 'roles'],
junctionTable: true,
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
user: {
columnName: 'user',
type: 'integer',
foreignKey: true,
references: 'user',
on: 'id',
via: 'role',
groupBy: 'user'
},
roles: {
columnName: 'role',
type: 'integer',
foreignKey: true,
references: 'role',
on: 'id',
via: 'user',
groupBy: 'role'
}
}
}
But when I trying to access roles atribute in controller
User.findOne({id: 1}).populate('roles').exec(function(err, user) {
console.log(JSON.stringify(user.roles));
});
this returns
[]
And
User.findOne({id: 1}).populate('roles').exec(function(err, user) {
console.log(JSON.stringify(user));
});
returns
{"id":1,"name":"test", "roles":[]}
I checked twice that user, role and association between them exists in database. What is my mistake?
I have found way to solve this problem. It's not what I exactly want, but it works.
First: join entity:
//Policy.js
module.exports = {
tableName: "users_roles",
autoPK: false,
attributes: {
id: {
type: 'integer',
primaryKey: true,
autoIncrement: true,
},
user: {
columnName: 'user_id',
model: 'user'
},
role: {
columnName: 'role_id',
model: 'role'
}
},
//tricky method to get all users for specified role_id
//or to get all roles for specified user_id
get: function(id, modificator, cb) {
var fields = ['user', 'role'];
if (fields.indexOf(modificator) < 0) {
cb(new Error('No such modificator in Policy.get()'), null);
}
var inversedField = fields[(fields.indexOf(modificator) + 1) % 2];
var condition = {};
condition[inversedField] = id;
this.find(condition).populate(modificator).exec(function(err, policies) {
if (err) {
cb(err, null);
return;
}
var result = [];
policies.forEach(function(policy) {
result.push(policy[modificator]);
});
cb(null, result);
return;
});
}
}
As you see, I added ID field to this entity (and to db table users_roles too), so it's not the great solution.
//User.js
module.exports = {
tableName: 'users',
autoPK: false,
attributes: {
id: {
type: 'integer',
primaryKey: true,
autoIncrement: true,
unique: true,
},
name: {
type: 'string'
},
policies: {
collection: 'policy',
via: 'user'
}
}
}
And Role Entity:
//Role.js
module.exports = {
tableName: 'roles',
autoPK: false,
attributes: {
id: {
type: 'integer',
primaryKey: true,
autoIncrement: true,
},
alias: {
type: 'string',
required: true,
unique: true,
},
policies: {
collection: 'policy',
via: 'role'
}
}
}
That's how I get all roles for specified user_id:
...
id = req.session.me.id; //user_id here
Policy.get(id, 'role', function(err, roles) {
var isAdmin = false;
roles.forEach(function(role) {
isAdmin |= (role.id === 1);
});
if (isAdmin) {
next(null);
return;
} else {
return res.redirect('/login');
}
});
...
Maybe it'll be usefull for somebody =)