Get number of associated elements in Sequelize - node.js

So I have a many-to-many relationship with Sequelize. This code gives me an array of all the categories associated with the Post. It works to get this data. However, if I would like to make that list of categories into just a single key value pair of how many categories instead of the categories. How could I do that?
return models.Post.findAndCountAll({
limit: limit,
offset: offset,
include: [{
model: models.Category,
as: 'categories',
required: false,
}],
})
For example this is the current output:
{
"id": 1,
"name": "foo",
"categories": [
{
"id": 1,
"name": "bar"
}
]
}
The desired output:
{
"id": 1,
"name": "foo",
"categories": 10
}
EDIT: As suggestions for fixing this I tried doing this:
return models.Post.findAndCountAll({
group: ['post.id'],
attributes: {
include: [[db.sequelize.fn("COUNT", db.sequelize.col("categories.id")), "categoriesCount"]]
},
limit: limit,
offset: offset,
include: [{
model: models.Category,
as: 'categories',
required: true,
attributes: []
}],
raw: true,
subQuery: false
})
But that just gives me the error:
{
"message": "invalid reference to FROM-clause entry for table \"post\""
}
This is basically what I want to get back, i wrote it in SQL and tried it:
SELECT
cp.category_id as category_id,
p.name as post_name,
COUNT(p.id) as num_categories
FROM
category c,
category_post cp
JOIN
post p ON p.id = cp.category_id
WHERE
p.id = cp.post_id AND
p.created_at >= '2022-01-26' and p.created_at <= '2022-05-02'
GROUP BY
cp.category_id,
post_name
ORDER BY
num_categories DESC
Generated SQL with Sequelize:
Executing (default): SELECT "post"."id", count("post"."id") AS "count" FROM "post" AS "post" INNER JOIN ( "category_post" AS "categories->categoryPost" INNER JOIN "category" AS "categories" ON "categories"."id" = "categories->categoryPost"."category_id") ON "post"."id" = "categories->categoryPost"."post_id" GROUP BY "post"."id";
Executing (default): SELECT "post"."id", "post"."name", COUNT("categories"."id") AS "categoryCount", "categories->categoryPost"."id" AS "categories.categoryPost.id", "categories->categoryPost"."category_id" AS "categories.categoryPost.category_id", "categories->categoryPost"."post_id" AS "categories.categoryPost.post_id" FROM "post" AS "post" INNER JOIN ( "category_post" AS "categories->categoryPost" INNER JOIN "category" AS "categories" ON "categories"."id" = "categories->categoryPost"."category_id") ON "post"."id" = "categories->categoryPost"."post_id" GROUP BY "post"."id" LIMIT 3 OFFSET 0;
My models look like the following:
Post(id, name, created_at, updated_at)
Category(id, name,)
PostCategory(id, post_id,category_id)
In my Post model:
static associate(models) {
this.belongsToMany(models.Category, {
through: models.CategoryPost,
as: 'posts',
foreignKey: 'category_id',
onDelete: 'CASCADE'
})
}
In my Category model:
static associate(models) {
this.belongsToMany(models.Post, {
through: models.CategoryPost,
as: 'categories',
foreignKey: 'post_id',
onDelete: 'CASCADE'
})
}
The generated SQL based on Emma's answer:
Executing (default): SELECT count("Post"."id") AS "count" FROM "Post" AS "Post" INNER JOIN ( "category_Post" AS "categories->categoryPost" INNER JOIN "category" AS "categories" ON "categories"."id" = "categories->categoryPost"."category_id") ON "Post"."id" = "categories->categoryPost"."Post_id";
Executing (default): SELECT "Post"."id", "Post"."name", (COUNT("categories"."id") OVER (PARTITION BY "Post"."id")::int) AS "categories" FROM "Post" AS "Post" INNER JOIN ( "category_Post" AS "categories->categoryPost" INNER JOIN "category" AS "categories" ON "categories"."id" = "categories->categoryPost"."category_id") ON "Post"."id" = "categories->categoryPost"."Post_id" LIMIT 3 OFFSET 0;

If Post is the parent and Category is the child and you want to find the number of categories for a given post... you can u se the following way..
return models.Post.findAll({
attributes: {
include: [[Sequelize.fn("COUNT", Sequelize.col("categories.id")), "cetegoryCount"]]
},
include: [{
model: models.Category,
as: 'categories'
}],
})

group in Postgres usually have some issues(Aggregate funcction issues).
Alternatively, you can use OVER PARTITION BY syntax which usually works in this situation.
const posts = await models.Post.findAndCountAll({
attributes: {
include: [[db.sequelize.literal('(COUNT("categories"."id") OVER (PARTITION BY "post"."id")::int)'), 'categories']]
},
limit: limit,
offset: offset,
include: [{
model: models.Category,
as: 'categories',
required: true,
attributes: [],
through: {
attributes: []
}
}],
raw: true,
subQuery: false
})
This should return something like this.
{
"result": {
"count": 2, // This count is for post
"rows": [
{
"id": 1,
"name": "post",
"categories": 2,
...
}
]
}
}

Related

How do I group by a column in a nested association in sequelize.js?

I'm trying to implement the following SQL query using sequelize.js:
select
practtype.description,
SUM("chargevalue") as "categoryvalue"
from
"public"."profees" as "profees"
inner join "public"."practitioners" AS "practitioner" on
"profees"."practitionerid" = "practitioner"."practitionerid"
inner join public.practtypes as "practtype" on
"practitioner"."practtypeid" = "practtype"."practtypeid"
group by
practtype.description
order by
practtype.description;
I have the following associations set up in my init-models.js component:
profees.belongsTo(practitioners, { as: "practitioner", foreignKey: "practitionerid"});
practitioners.hasMany(profees, { as: "profees", foreignKey: "practitionerid"});
practitioners.belongsTo(practtypes, { as: "practtype", foreignKey: "practtypeid"});
practtypes.hasMany(practitioners, { as: "practitioners", foreignKey: "practtypeid"});
I'm struggling to find decent documentation or examples of how to specify the group clause of my sequelize query. Here's what I currently have:
const feesSummary = await profees.findAll({
attributes: [
[sequelize.fn("SUM", sequelize.col("chargevalue")), "categoryvalue"],
// ["chargevalue", "categoryvalue"],
],
include: [
{
model: practitioners,
as: "practitioner",
required: true,
attributes: ["surname"],
include: [
{
model: practtypes,
as: "practtype",
required: true,
attributes: ["description"],
},
],
},
],
group: [
[
{ model: practitioners, as: "practitioner" },
{ model: practtypes, as: "practtype" },
"description",
],
],
order: [
[
{ model: practitioners, as: "practitioner" },
{ model: practtypes, as: "practtype" },
"description",
"ASC",
],
],
// where: sequelize.where(sequelize.fn("date_part", ["year", sequelize.col("chargedate")]), year)
});
(I've commented out the where clause for the time being, as it's also giving me separate issues).
When I try to execute the sequelize query above, I get the following error:
Error: Unknown structure passed to order / group: { model: practitioners, as: 'practitioner' }
at PostgresQueryGenerator.quote (/home/developer/medexpenses/medexps-service/node_modules/sequelize/lib/dialects/abstract/query-generator.js:888:11)
at PostgresQueryGenerator.aliasGrouping (/home/developer/medexpenses/medexps-service/node_modules/sequelize/lib/dialects/abstract/query-generator.js:1418:17)
at /home/developer/medexpenses/medexps-service/node_modules/sequelize/lib/dialects/abstract/query-generator.js:1343:82
at Array.map (<anonymous>)
at PostgresQueryGenerator.selectQuery (/home/developer/medexpenses/medexps-service/node_modules/sequelize/lib/dialects/abstract/query-generator.js:1343:68)
at PostgresQueryInterface.select (/home/developer/medexpenses/medexps-service/node_modules/sequelize/lib/dialects/abstract/query-interface.js:954:27)
at Function.findAll (/home/developer/medexpenses/medexps-service/node_modules/sequelize/lib/model.js:1753:47)
I'm using version 6.6.2 of sequelize.
Can anyone post an example of the correct way to formulate the group clause in the query above? The Sequelize documentation suggests that
The syntax for grouping and ordering are equal, except that grouping does not accept a direction as last argument of the array (there is no ASC, DESC, NULLS FIRST, etc).
which is why I formulated the group clause as I did, but I'm getting the error above nonetheless.

Sequelize Subqueries

in this case i want to find the total prepare time for each order according the orderId, but when i write like this it only shows 1 result which is the first one,
let prepareOrder = await OrderItem.findAll({
where: {
options: null,
},
attributes: ["orderId"],
include: [
{
model: Item,
attributes: [
[
sequelize.fn("sum", sequelize.col("prepareTime")),
"totalPrepareTime",
],
],
},
],
});
You need to rum SUM() on the outer query. When you run it on the inner query it is returning a single row and then doing a JOIN which is why you only get one row.
const prepareOrder = await OrderItem.findAll({
attributes: [
"orderId",
// get the summary at the OrderItem for each related Item
[
sequelize.fn("sum", sequelize.col("item.prepareTime")),
"totalPrepareTime",
],
],
// include Item but no attributes for performance
include: {
model: Item,
attributes: [],
},
where: {
options: null,
},
});

How can I join same table using sequalizer?

I am using sequalizer first time and stuck in a situation
I have a table categories as below
I need details like
[{
"id" : 7,
"name": "Mobile Cover",
parent: {
"id": 1
"name": "Mobile",
}
},
{
"id" : 9,
"name": "Mobile Glass",
parent: {
"id": 1
"name": "Mobile",
}
},
{
"id" : 8,
"name": "Knife",
parent: {
"id": 4
"name": "Kitchenware",
}
}]
I try with include but it's not working
const result = await category.findAll({
where: {
parentId: {
[Op.ne]: null
}
},
include: {
model: category,
where: {
'id': 'category.parentId'
}
},
order: [
["createdAt", "DESC"],
],
});
it's returning error
SequelizeDatabaseError: table name "categories" specified more than
once
Is there any way to do this?
Sequelize version: "sequelize": "^5.21.3". Here is a working example:
index.ts:
import { sequelize } from '../../db';
import Sequelize, { Model, DataTypes, Op } from 'sequelize';
class Category extends Model {}
Category.init(
{
id: {
primaryKey: true,
autoIncrement: true,
allowNull: false,
type: DataTypes.INTEGER,
},
name: DataTypes.STRING,
},
{ sequelize, modelName: 'categories' },
);
Category.belongsTo(Category, { foreignKey: 'parentId', as: 'parent', targetKey: 'id' });
(async function test() {
try {
await sequelize.sync({ force: true });
// seed
const parent1 = { id: 1, name: 'Mobile' };
const parent2 = { id: 4, name: 'Kitchenware' };
await Category.bulkCreate([parent1, parent2]);
await Category.bulkCreate([
{ id: 7, name: 'Mobile Cover', parentId: parent1.id },
{ id: 8, name: 'Knife', parentId: parent2.id },
{ id: 9, name: 'Mobile Glass', parentId: parent1.id },
]);
// test
const result = await Category.findAll({
include: [
{
model: Category,
required: true,
as: 'parent',
attributes: ['id', 'name'],
},
],
attributes: ['id', 'name'],
raw: true,
});
console.log('result: ', result);
} catch (error) {
console.log(error);
} finally {
await sequelize.close();
}
})();
The execution results:
Executing (default): DROP TABLE IF EXISTS "categories" CASCADE;
Executing (default): DROP TABLE IF EXISTS "categories" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "categories" ("id" SERIAL , "name" VARCHAR(255), "parentId" INTEGER REFERENCES "categories" ("id") ON DELETE SET NULL ON UPDATE CASCADE, PRIMARY KEY ("id"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'categories' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): INSERT INTO "categories" ("id","name") VALUES (1,'Mobile'),(4,'Kitchenware') RETURNING *;
Executing (default): INSERT INTO "categories" ("id","name","parentId") VALUES (7,'Mobile Cover',1),(8,'Knife',4),(9,'Mobile Glass',1) RETURNING *;
Executing (default): SELECT "categories"."id", "categories"."name", "parent"."id" AS "parent.id", "parent"."name" AS "parent.name" FROM "categories" AS "categories" INNER JOIN "categories" AS "parent" ON "categories"."parentId" = "parent"."id";
result: [ { id: 7,
name: 'Mobile Cover',
'parent.id': 1,
'parent.name': 'Mobile' },
{ id: 8,
name: 'Knife',
'parent.id': 4,
'parent.name': 'Kitchenware' },
{ id: 9,
name: 'Mobile Glass',
'parent.id': 1,
'parent.name': 'Mobile' } ]
Check the data records in the database:
node-sequelize-examples=# select * from "categories";
id | name | parentId
----+--------------+----------
1 | Mobile |
4 | Kitchenware |
7 | Mobile Cover | 1
8 | Knife | 4
9 | Mobile Glass | 1
(5 rows)
You need to define an association as follows -
Category.belongsTo(Category, {
foreignKey: "parentID",
targetKey: "id"
});
Before using include you need to define associations on the basis of their relations. Sequelize has all traditional types of relationships like hasMany, BelogTo, etc.
furthermore check out the link: https://sequelize.org/master/manual/associations.html

Sequelize include same association twice with different criteria

Say I have a table suppliers, which is associated to tags through another table suppliers_tags. So my model looks like this:
const Supplier = Sequelize.define('suppliers', {
id: { type: Sequelize.INTEGER, primaryKey: true },
name: Sequelize.STRING,
});
const SupplierTag = Sequelize.define('suppliers_tags', {
id: { type: Sequelize.INTEGER, primaryKey: true },
supplier_id: { type: Sequelize.INTEGER, references: { model: 'suppliers', key: 'id' } },
tag_id: { type: Sequelize.INTEGER, references: { model: 'suppliers', key: 'id' } },
});
const Tag = Sequelize.define('tags', {
id: { type: Sequelize.INTEGER, primaryKey: true },
name: Sequelize.STRING,
type: Sequelize.ENUM('food', 'drink'),
});
The associations look like this:
Supplier.belongsToMany(Tag, { as: 'tags', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Tag.belongsToMany(Supplier, { as: 'supplierTag', through: 'suppliers_tags', foreignKey: 'tag_id' });
Let's say I have the following data in the database:
suppliers:
id name
1 Supplier1
2 Supplier2
3 Supplier3
tags:
id name type
1 Food1 food
2 Vegan food
3 Vegan drink
4 Food2 food
(I purposely named two tags the same, as for this application, it is important that tags with different types can have the same name.)
suppliers_tags
id supplier_id tag_id
1 1 1
2 1 3
3 1 2
4 2 1
5 2 4
6 3 2
Now I can do the following query:
Supplier.findAll({
include: [
{
model: Tag,
as: 'tags',
where: {
[Op.and]: [
{ type: 'food' },
{ name: 'Vegan' },
],
},
},
],
});
This returns Supplier1 and Supplier3, correctly joining on suppliers_tags and tags and filtering the tags table to include ones of type 'food' and name 'Vegan'.
Now, what if I want to search for suppliers where both the following conditions hold:
The supplier has an associated tag which is of type food and name Food1
The supplier has an associated tag which is of type drink and name Vegan
Naively (?), I tried the following:
Supplier.findAll({
include: [
{
model: Tag,
as: 'tags_food',
where: {
[Op.and]: [
{ type: 'food' },
{ name: 'Food1' },
],
},
},
{
model: Tag,
as: 'tags_drink',
where: {
[Op.and]: [
{ type: 'drink' },
{ name: 'Vegan' },
],
},
},
],
});
This tries to join the tags table twice, but neglecting to add aliases, resulting in the following error:
SequelizeEagerLoadingError: tags is associated to suppliers multiple times. To identify the correct association, you must use the 'as' keyword to specify the alias of the association you want to include.
Right, so it seems like the as options on the include didn't have the desired effect. What if I modify the associations to be the following:
Supplier.belongsToMany(Tag, { as: 'tags_drink', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Supplier.belongsToMany(Tag, { as: 'tags_food', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Tag.belongsToMany(Supplier, { as: 'supplierTag', through: 'suppliers_tags', foreignKey: 'tag_id' });
Now, if I run the same findAll query, it generates the following SQL:
SELECT
"suppliers".*
,"tags_food"."id" AS "tags_food.id"
,"tags_food"."name" AS "tags_food.name"
,"tags_food"."type" AS "tags_food.type"
,"tags_food->suppliers_tags"."supplier_id" AS "tags_food.suppliers_tags.supplier_id"
,"tags_food->suppliers_tags"."tag_id" AS "tags_food.suppliers_tags.tag_id"
,"tags_drink"."id" AS "tags_drink.id"
,"tags_drink"."name" AS "tags_drink.name"
,"tags_drink"."type" AS "tags_drink.type"
,"tags_drink->suppliers_tags"."supplier_id" AS "tags_drink.suppliers_tags.supplier_id"
,"tags_drink->suppliers_tags"."tag_id" AS "tags_drink.suppliers_tags.tag_id"
FROM (
SELECT
"suppliers"."id"
,"suppliers"."name"
FROM "suppliers" AS "suppliers"
WHERE (
SELECT "suppliers_tags"."supplier_id"
FROM "suppliers_tags" AS "suppliers_tags"
INNER JOIN "tags" AS "tag" ON "suppliers_tags"."tagId" = "tag"."id"
AND ("tag"."type" = 'food' AND "tag"."name" = 'Food1')
WHERE ("suppliers"."id" = "suppliers_tags"."supplier_id")
LIMIT 1
) IS NOT NULL
AND (
SELECT "suppliers_tags"."supplier_id"
FROM "suppliers_tags" AS "suppliers_tags"
INNER JOIN "tags" AS "tag" ON "suppliers_tags"."tag_id" = "tag"."id"
AND ("tag"."type" = 'drink' AND "tag"."name" = 'Vegan')
WHERE ("suppliers"."id" = "suppliers_tags"."supplier_id")
LIMIT 1
) IS NOT NULL
) AS "suppliers"
INNER JOIN (
"suppliers_tags" AS "tags_food->suppliers_tags"
INNER JOIN "tags" AS "tags_food"
ON "tags_food"."id" = "tags_food->suppliers_tags"."tagId"
) ON "suppliers"."id" = "tags_food->suppliers_tags"."supplier_id"
AND ("tags_food"."type" = 'food' AND "tags_food"."name" = 'Food1')
INNER JOIN (
"suppliers_tags" AS "tags_drink->suppliers_tags"
INNER JOIN "tags" AS "tags_drink"
ON "tags_drink"."id" = "tags_drink->suppliers_tags"."tag_id"
) ON "suppliers"."id" = "tags_drink->suppliers_tags"."supplier_id"
AND ("tags_drink"."type" = 'drink' AND "tags_drink"."name" = 'Vegan')
ORDER BY "suppliers"."id"
This is correct and what I need, except for one error: the foreign key tag_id has been switched to tagId (?!) in the case of the tags_food join. This then of course fails with the following error:
SequelizeDatabaseError: column tags_food->suppliers_tags.tagId does not exist
Notice that the correct foreign key tag_id was generated in the join on tags_drink by Sequelize.
If I switch around the relation definitions, i.e.:
Supplier.belongsToMany(Tag, { as: 'tags_food', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Supplier.belongsToMany(Tag, { as: 'tags_drink', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Tag.belongsToMany(Supplier, { as: 'supplierTag', through: 'suppliers_tags', foreignKey: 'tag_id' });
Then tag_id is produced successfully for the tags_food join and the weird tagId is produced for the tags_drink join.
Can I put this down to another basic flaw with Sequelize, or:
Is there a defined way of joining the same relation twice on different criteria in Sequelize?

unknown column in field list sequelize

I'm trying to perform the following query using Sequelize:
db.Post.findAll({
include: [
{
model: db.User,
as: 'Boosters',
where: {id: {[Op.in]: a_set_of_ids }}
},
{
model: db.Assessment,
as: 'PostAssessments',
where: {UserId: {[Op.in]: another_set_of_ids}}
}
],
attributes: [[db.sequelize.fn('AVG', db.sequelize.col('Assessments.rating')), 'average']],
where: {
average: 1
},
group: ['id'],
limit: 20
})
But I run to this error: "ER_BAD_FIELD_ERROR". Unknown column 'Assessments.rating' in 'field list', although I do have table "Assessments" in the database and "rating" is a column in that table.
My Post model looks like this:
const Post = sequelize.define('Post', {
title: DataTypes.TEXT('long'),
description: DataTypes.TEXT('long'),
body: DataTypes.TEXT('long')
}, {
timestamps: false
});
Post.associate = function (models) {
models.Post.belongsToMany(models.User, {as: 'Boosters', through: 'UserPostBoosts' });
models.Post.hasMany(models.Assessment, {as: 'PostAssessments'});
};
What am I doing wrong?
It seems like this problem surfaces when we have a limit in a find query where associated models are included (the above error doesn't show up when we drop the limit from the query). To solve that, we can pass an option subQuery: false to the find. (https://github.com/sequelize/sequelize/issues/4146)
This is the correct query in case anyone comes across the same problem:
db.Post.findAll({
subQuery: false,
include: [
{
model: db.User,
as: 'Boosters',
where: {id: {[Op.in]: a_set_of_ids }}
}
,{
model: db.Assessment,
as: 'PostAssessments',
where: {UserId: {[Op.in]: another_set_of_ids}}
}
],
having: db.sequelize.where(db.sequelize.fn('AVG', db.sequelize.col('PostAssessments.rating')), {
[Op.eq]: 1,
}),
limit: 20,
offset: 2,
group: ['Post.id', 'Boosters.id', 'PostAssessments.id']
})
Error is with this one :
models.sequelize.col('Assessments.rating'))
Change it to
models.sequelize.col('PostAssessments.rating')) // or post_assessments.rating
Reason : You are using the alias for include as: 'PostAssessments',.

Resources