Sub Queries

Consider you have two models, Post and Reaction, with a One-to-Many relationship set up, so that one post has many reactions:

  1. const Post = sequelize.define('post', {
  2. content: DataTypes.STRING
  3. }, { timestamps: false });
  4. const Reaction = sequelize.define('reaction', {
  5. type: DataTypes.STRING
  6. }, { timestamps: false });
  7. Post.hasMany(Reaction);
  8. Reaction.belongsTo(Post);

Note: we have disabled timestamps just to have shorter queries for the next examples.

Let’s fill our tables with some data:

  1. async function makePostWithReactions(content, reactionTypes) {
  2. const post = await Post.create({ content });
  3. await Reaction.bulkCreate(
  4. reactionTypes.map(type => ({ type, postId: post.id }))
  5. );
  6. return post;
  7. }
  8. await makePostWithReactions('Hello World', [
  9. 'Like', 'Angry', 'Laugh', 'Like', 'Like', 'Angry', 'Sad', 'Like'
  10. ]);
  11. await makePostWithReactions('My Second Post', [
  12. 'Laugh', 'Laugh', 'Like', 'Laugh'
  13. ]);

Now, we are ready for examples of the power of subqueries.

Let’s say we wanted to compute via SQL a laughReactionsCount for each post. We can achieve that with a sub-query, such as the following:

  1. SELECT
  2. *,
  3. (
  4. SELECT COUNT(*)
  5. FROM reactions AS reaction
  6. WHERE
  7. reaction.postId = post.id
  8. AND
  9. reaction.type = "Laugh"
  10. ) AS laughReactionsCount
  11. FROM posts AS post

If we run the above raw SQL query through Sequelize, we get:

  1. [
  2. {
  3. "id": 1,
  4. "content": "Hello World",
  5. "laughReactionsCount": 1
  6. },
  7. {
  8. "id": 2,
  9. "content": "My Second Post",
  10. "laughReactionsCount": 3
  11. }
  12. ]

So how can we achieve that with more help from Sequelize, without having to write the whole raw query by hand?

The answer: by combining the attributes option of the finder methods (such as findAll) with the sequelize.literal utility function, that allows you to directly insert arbitrary content into the query without any automatic escaping.

This means that Sequelize will help you with the main, larger query, but you will still have to write that sub-query by yourself:

  1. Post.findAll({
  2. attributes: {
  3. include: [
  4. [
  5. // Note the wrapping parentheses in the call below!
  6. sequelize.literal(`(
  7. SELECT COUNT(*)
  8. FROM reactions AS reaction
  9. WHERE
  10. reaction.postId = post.id
  11. AND
  12. reaction.type = "Laugh"
  13. )`),
  14. 'laughReactionsCount'
  15. ]
  16. ]
  17. }
  18. });

Important Note: Since sequelize.literal inserts arbitrary content without escaping to the query, it deserves very special attention since it may be a source of (major) security vulnerabilities. It should not be used on user-generated content. However, here, we are using sequelize.literal with a fixed string, carefully written by us (the coders). This is ok, since we know what we are doing.

The above gives the following output:

  1. [
  2. {
  3. "id": 1,
  4. "content": "Hello World",
  5. "laughReactionsCount": 1
  6. },
  7. {
  8. "id": 2,
  9. "content": "My Second Post",
  10. "laughReactionsCount": 3
  11. }
  12. ]

Success!

Using sub-queries for complex ordering

This idea can be used to enable complex ordering, such as ordering posts by the number of laugh reactions they have:

  1. Post.findAll({
  2. attributes: {
  3. include: [
  4. [
  5. sequelize.literal(`(
  6. SELECT COUNT(*)
  7. FROM reactions AS reaction
  8. WHERE
  9. reaction.postId = post.id
  10. AND
  11. reaction.type = "Laugh"
  12. )`),
  13. 'laughReactionsCount'
  14. ]
  15. ]
  16. },
  17. order: [
  18. [sequelize.literal('laughReactionsCount'), 'DESC']
  19. ]
  20. });

Result:

  1. [
  2. {
  3. "id": 2,
  4. "content": "My Second Post",
  5. "laughReactionsCount": 3
  6. },
  7. {
  8. "id": 1,
  9. "content": "Hello World",
  10. "laughReactionsCount": 1
  11. }
  12. ]