Get the count of Associated tables using sequelize - node.js

In my Case Account is a parent table Account that has many Members, an Account has many Groups, Account has many Roles. So I tried getting a count of each table as follows.
Account.findAll({
subQuery: false,
where: {id: id},
attributes: {
include: ['*',
[Sequelize.fn("COUNT", Sequelize.col("Members.id")), "usersCount"],
[Sequelize.fn("COUNT", Sequelize.col("Groups.id")), "groupsCount"],
[Sequelize.fn("COUNT", Sequelize.col("Roles.id")), "rolesCount"]]
},
include: [
{
model: Member, attributes: [], where: {accountId: id}
},
{
model: Group, attributes: [], where: {accountId: id}
},
{
model: Role, attributes: [], where: {accountId: id}
}
],
group: ['Account.id']
})
.then(data => {
let result = data.map(item => accountObj(item));
return next(null, result[0]);
})
.catch(err => dbHelper.handleError(err, next));
It's giving me an incorrect count. Also tried various other options I found on stack overflow. But not found any solution for the above query.

You either need to separate this query into three ones to count each child model record count separately OR remove include and group options and replace Sequelize.fn with Sequelize.literal with subqueries to count child records.
Account.findAll({
where: {id: id},
attributes: {
include: ['*',
[Sequelize.literal("(SELECT COUNT(*) from members as m where m.account_id=account.id)", "usersCount"],
[Sequelize.literal("(SELECT COUNT(*) from groups as g where g.account_id=account.id)", "groupsCount"],
[Sequelize.literal("(SELECT COUNT(*) from roles as r where r.account_id=account.id)", "rolesCount"],
]
}
})

Related

Sequelize join two tables on id

I have a table of book users and a table of movie users. I'm trying to return a list of the top 100 movie viewers, along with their book profile information. I want to join on ids, but I can't seem to find the right syntax.
This is what I've tried:
const mostActiveMovieWatchers = await MovieWatchers.findAll({
order: [
['moviesWatched', 'DESC'],
],
limit: '100',
include: [{
model: BookReaders,
where: {
userId: '$MovieWatchers.id$'
},
required: true
}]
});
I've also seen examples where the where clause looks something like this where: ['userId = id']
Before join tables you need create association:
BookReaders.hasMany(MovieWatchers, { foreignKey: 'bookId' });
MovieWatchers.belongsTo(BookReaders, { foreignKey: 'bookId' });
Then, you can use the include option in a find or findAll method call on the MovieWatchers model to specify that you want to include the associated BookReaders data:
MovieWatchers.findAll({
include: [
{
model: BookReaders,
},
],
}).then((movies) => {
// array of movies including books
});

Get all associated records given one match

I have two models with a one to many relation. Given that below query returns a record of ModelA (lets say it has id 1) with 3 associated ModelB (1, 2 and 3).
If I were to replace [1,2,3] in the query to just [1] it would still return the same ModelA record (with id 1) but only with the one associated ModelB (of id 1). How can I modify this query so it returns all three associated ModelB records?
ModelA.findAll({
include: [{
model: JoinTableModel,
where: {
modelB_ID: {
[Op.in]: [1,2,3]
}
},
include: [ModelB]
}]
})
Model definitions like so.
db.ModelA.hasMany(db.JoinTableModel, { foreignKey: 'modelA_ID' })
db.JoinTableModel.belongsTo(db.ModelA, { foreignKey: 'modelA_ID' })
db.ModelB.hasMany(db.JoinTableModel, { foreignKey: 'modelB_ID' })
db.JoinTableModel.belongsTo(db.ModelB, { foreignKey: 'modelB_ID' })
You can try something like this:
JoinTableModel.findAll({
where: {
modelB_ID: {
[Op.in]: [1]
}
},
include: [{
model: ModelA,
include: [{
model: JoinTableModel,
include: [ModelB]
}]
}]
})
If you add an association like this:
ModelA.belongsToMany(ModelB, { through: JoinTableModel })
then you can simplify the above query to this one:
JoinTableModel.findAll({
where: {
modelB_ID: {
[Op.in]: [1]
}
},
include: [{
model: ModelA,
include: [ModelB]
}]
})
For anyone with a similar issue as me, what I ended up doing that works for my case is modifying my initial query like this.
ModelA.findAll({
where: {
id: sequelize.literal(`
ModelA.id
IN (
SELECT modelA_ID FROM JoinTableModel
WHERE modelB_ID IN (1,2,3)
GROUP BY modelA_ID
HAVING COUNT(*) = 3
)`)
},
include: [{
model: JoinTableModel,
include: [ModelB]
}]
})
This way I could easily replace IN (1,2,3) and COUNT(*) 3 with IN (1) and COUNT(*) 1 and it would still work as intended and it doesn't break any other part of the query.
I'm still curious if anyone could solve this without using sequelize.literal in any way or if there is a more efficient way of doing it.

Sequelize - ORDER BY count of associations

I have two models, Company and Employee. The association is like Company hasMany Employee. Employee belongsTo Company.
I have to list all the Companies ordered by count of associations it have in the descending order.
My function is like following,
function get() {
return Company.findAndCountAll({
where: { status: Active },
include: [
{
model: Employee,
as: 'employees'
}
],
}).then((companies) => {
return companies
})
}
What can I give in order so that I can get list of companies based on count of associations(Employees) in descending order.
Please help me out in this...
I have tried as below. but it is throwing error like invalid reference to FROM-clause entry for table companies.
function get() {
return Company.findAndCountAll({
where: { status: Active },
include: [
{
model: Employee,
as: 'employees'
}
],
attributes: [
[Sequelize.literal('(SELECT COUNT(*) FROM employees WHERE employees.company_id = companies.id)'), 'employeesCount']
],
order: [[Sequelize.literal('employeesCount'), 'DESC']],
}).then((companies) => {
return companies
})
}
where companies is the table name.
You can try this.
Please replace your fieldName with sortField
function get() {
return Company.findAndCountAll({
where: { status: Active },
include: [
{
model: Employee,
as: 'employees'
}
],
order: [[sortField, 'DESC']]
}).then((companies) => {
return companies
})
}

Sequelize How to delete an occurence in an association table

I'm having two tables user and group, a manytomany relation between them, thereofre a third table group_has_user
given a user I'm trying to remove some groups,
I tried :
Model.User.findOne({
where: {
"id": POST.id
},
include: [Model.Project]
}).then(function (user) {
user.Project.destroy({
where:{
"id": ids
}
})
})
where id is the user id and ids, is a list of groups that I wan to
remove
but this code doesn't works, project is undefined, and also I don't think that what I want could be done this way, can anyone help ?
You can get the Projects you want to remove and then call the generated removeProject method on each project you got:
Model.User.findOne({
where: {
"id": POST.id
},
include: [Model.Project]
}).then(function (user) {
Model.Projcet.find({
where: {
id: {
[Op.in]: ids
}
}
}).then(projectsToRemove => {
projectsToRemove.forEach(p => {
await user.removeProject(p);
})
})
})
Source: Official documentation

How to count a group by query in NodeJS Sequelize

In Rails I can perform a simple ORM query for the number of Likes a model has:
#records = Model
.select( 'model.*' )
.select( 'count(likes.*) as likes_count' )
.joins( 'LEFT JOIN likes ON model.id = likes.model_id' )
.group( 'model.id' )
This generates the query:
SELECT models.*, count(likes.*) as likes_count
FROM "models" JOIN likes ON models.id = likes.model_id
GROUP BY models.id
In Node Sequelize, any attempt at doing something similar fails:
return Model.findAll({
group: [ '"Model".id' ],
attributes: ['id', [Sequelize.fn('count', Sequelize.col('"Likes".id')), 'likes_count']],
include: [{ attributes: [], model: Like }],
});
This generates the query:
SELECT
Model.id,
count(Likes.id) AS likes_count,
Likes.id AS Likes.id # Bad!
FROM Models AS Model
LEFT OUTER JOIN Likes
AS Likes
ON Model.id = Likes.model_id
GROUP BY Model.id;
Which generates the error:
column "Likes.id" must appear in the GROUP BY clause or be used in an aggregate function
It's erroneously selecting likes.id, and I have no idea why, nor how to get rid of it.
This sequelize github issue looks totally like your case:
User.findAll({
attributes: ['User.*', 'Post.*', [sequelize.fn('COUNT', 'Post.id'), 'PostCount']],
include: [Post]
});
To resolve this problem we Need to upgrade to latest version of sequelize and include raw = true,
Here is How I had done after lot of iteration and off-course googling.
getUserProjectCount: function (req, res) {
Project.findAll(
{
attributes: ['User.username', [sequelize.fn('COUNT', sequelize.col('Project.id')), 'ProjectCount']],
include: [
{
model: User,
attributes: [],
include: []
}
],
group: ['User.username'],
raw:true
}
).then(function (projects) {
res.send(projects);
});
}
where my reference models are
//user
var User = sequelize.define("User", {
username: Sequelize.STRING,
password: Sequelize.STRING
});
//project
var Project = sequelize.define("Project", {
name: Sequelize.STRING,
UserId:{
type:Sequelize.INTEGER,
references: {
model: User,
key: "id"
}
}
});
Project.belongsTo(User);
User.hasMany(Project);
after migration ORM create 'Users' & 'Projects' table into my postgres server.
Here is SQL Query by ORM
SELECT
"User"."username", COUNT("Project"."id") AS "ProjectCount"
FROM
"Projects" AS "Project"
LEFT OUTER JOIN "Users" AS "User" ON "Project"."UserId" = "User"."id"
GROUP BY
"User"."username";
What worked for me counting column A and grouping by column B
const noListingsPerRetailer = Listing.findAll({
attributes: [
'columnA',
[sequelize.fn('COUNT', sequelize.col('columnB')), 'labelForCountColumn'],
],
group:["columnA"]
});

Resources