1

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;
  • how come you add the `group`? `group` by `post.id` doesn't make sense to me. also `required: false` is fine. it depends whether you want the post id which has 0 categories or not. – Emma May 03 '22 at 16:57
  • if you don't need the post counts then you should change `findAndCountAll` to `findAll`, – Emma May 03 '22 at 17:00
  • I tried uncommenting that one and I need to use findAndCountAll so I can still count all the categories that exists. –  May 03 '22 at 17:02
  • `findAndCountAll` is for counting the `Post`. and to count the `categories`, you have `Sequelize.fn('COUNT',`. So if you only care for the category's count, you should use `findAll` – Emma May 03 '22 at 17:04
  • Yeah sorry, my mistake, but it still valid. I want to count the posts. –  May 03 '22 at 17:06
  • Got it. Could you post the current generated SQL? – Emma May 03 '22 at 17:06
  • @Emma Updated the issue. –  May 03 '22 at 17:17

2 Answers2

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'
        }],
    })
Venkatesh A
  • 1,875
  • 1
  • 19
  • 23
  • Thanks for answer! However when running that I get: `{ "message": "missing FROM-clause entry for table \"categories\"" }` –  May 03 '22 at 15:06
  • oh okay.. can you try removing the required option? – Venkatesh A May 03 '22 at 16:19
  • it's close. You need the `required` option to get in case count is 0 but the error indicates that you need to have `subQuery` option. Plus I bet this requires `raw: true` option as well so that the count appears as raw format. Also, add the `attributes: []` within `include` to suppress the info from the associations. – Emma May 03 '22 at 16:31
  • @Emma Thanks, I updated the question with the result. It gives me some other error then. –  May 03 '22 at 16:50
0

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,
        ...
      }
    ]
  }
}
Emma
  • 8,518
  • 1
  • 18
  • 35
  • Thanks Emma for taking your time, but however I get this error then: `{ "message": "invalid reference to FROM-clause entry for table \"post\"" }` There must be something I'm doing wrong. I have updated the question with what code that does the associations. –  May 03 '22 at 18:35
  • I need to see the generated SQL with the new code. could you have that as well? – Emma May 03 '22 at 18:50
  • I updated the question with the output. –  May 04 '22 at 07:57
  • Hmm, have you changed associations or table configurations? In the error you show `...for table "post"` and previous SQL has `post` or `post_id` in generated SQL. But the latest generated SQL shows `Post` or `Post_id`. unfortunately, these small things often matter in sequelize/SQL, so I wanted to make sure. I like to see the generated SQL and matching error message from the generated SQL. – Emma May 04 '22 at 14:57
  • yes I have before, but then I just ran `sequelize db:migrate:undo:all` and then `sequelize db:migrate` to fix it. –  May 04 '22 at 16:02
  • If you could perhaps show me an example of how to write these relations with the models I could compare and see if I missed something. –  May 04 '22 at 16:04
  • I think it is easier to go from the error message to point out where the error is coming from. Do you have both latest generated SQL and the error message from the same execution? – Emma May 04 '22 at 16:13