1

I have the following tables:

Article - User - Tag - Followers - Suscribes

Article belongs to User (fk: userId in Article table)

Article can have many tag. Here is the generated tagarticle table:

enter image description here

Here is followers table:

enter image description here

And the Suscribes table:

enter image description here

A user can follow many users and suscribe to a country(payId), a tag or an article(for notifications).

How to query all articles of followed users and suscribed country or tag for a specific user?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Amadou Beye
  • 2,538
  • 3
  • 16
  • 37

2 Answers2

1

I assume that you ask about Sequelize way of doing the query. I am not sure if I understand your question correctly. You are looking for two queries:

  • Query all articles of followed users,
  • Query subscribed country/tag/article of specific user,

Let me start with the associations made between the models.

// in User model definition
User.belongsToMany(User, { as: 'Followers', through: 'Followers', foreignKey: 'userId', otherKey: 'followId' });
User.hasMany(Subscribe, { foreignKey: 'userId' });
User.hasMany(Article, { foreignKey: 'userId' });

With use of above association we are now able to query all articles of followed users

models.User.findByPrimary(1, {
    include: [
        {
            model: models.User,
            as: 'Followers',
            include: [ models.Article ]
        }
    ]
}).then(function(user){
    // here you have user with his followers and their articles
});

Above query would generate result similar to

{
    id: 1,
    Followers: [
        {
            id: 4,
            Articles: [
                {
                    id: 1,
                    title: 'article title' // some example field of Article model
                }
            ]
        }
    ]
}

If you want to query country/tag/article subscribed by specific user, you would have to make another associations in Subscribe model

// in Subscribe model definition
Subscribe.belongsTo(Tag, { foreignKey: 'tagId' });
Subscribe.belongsTo(Article, { foreignKey: 'articleId' });
Subscribe.belongsTo(Country, { foreignKey: 'payId' });

Now we have all the associations required to perform the second query you asked for

models.User.findByPrimary(1, {
    include: [
        {
            model: models.Subscribe,
            include: [ models.Tag, models.Country, models.Article ]
        }
    ]
}).then(function(user){
    // here you get user with his subscriptions
});

In this example you get user with all his subscriptions accessed via user.Subscribes, which will have nested attributes Tag, Country and Article. If user subscribed to Tag, both Country and Article would be NULL in this case.

piotrbienias
  • 7,201
  • 1
  • 28
  • 33
1

Minimal runnable example of getting articles by followed users with assertions

https://stackoverflow.com/a/42634024/895245 was correct, here is a runnable version of it, also covering some other related functionality like limit and ordering. Further examples of interest at: How to implement many to many association in sequelize tested on:

npm install sequelize@6.5.1 sqlite3@5.0.2

Source:

#!/usr/bin/env node

const assert = require('assert');
const path = require('path');

const { Sequelize, DataTypes } = require('sequelize');

const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: 'db.sqlite3',
});

(async () => {

// Create the tables.
const User = sequelize.define('User', {
  name: { type: DataTypes.STRING },
}, {});
const Post = sequelize.define('Post', {
  body: { type: DataTypes.STRING },
}, {});
User.belongsToMany(User, {through: 'UserFollowUser', as: 'Follows'});
User.hasMany(Post);
Post.belongsTo(User);
await sequelize.sync({force: true});

// Create data.
const users = await User.bulkCreate([
  {name: 'user0'},
  {name: 'user1'},
  {name: 'user2'},
  {name: 'user3'},
])

const posts = await Post.bulkCreate([
  {body: 'body00', UserId: users[0].id},
  {body: 'body11', UserId: users[0].id},
  {body: 'body10', UserId: users[1].id},
  {body: 'body11', UserId: users[1].id},
  {body: 'body20', UserId: users[2].id},
  {body: 'body21', UserId: users[2].id},
  {body: 'body30', UserId: users[3].id},
  {body: 'body31', UserId: users[3].id},
])

await users[0].addFollows([users[1], users[2]])

// Get all posts by authors that user0 follows.
// The posts are placed inside their respetive authors under .Posts
// so we loop to gather all of them.
{
  const user0Follows = (await User.findByPk(users[0].id, {
    include: [
      {
        model: User,
        as: 'Follows',
        include: [
          {
            model: Post,
          }
        ],
      },
    ],
  })).Follows
  const postsFound = []
  for (const followedUser of user0Follows) {
    postsFound.push(...followedUser.Posts)
  }
  postsFound.sort((x, y) => { return x.body < y.body ? -1 : x.body > y.body ? 1 : 0 })
  assert(postsFound[0].body === 'body10')
  assert(postsFound[1].body === 'body11')
  assert(postsFound[2].body === 'body20')
  assert(postsFound[3].body === 'body21')
  assert(postsFound.length === 4)
}

// With ordering, offset and limit.
// The posts are placed inside their respetive authors under .Posts
// The only difference is that posts that we didn't select got removed.

{
  const user0Follows = (await User.findByPk(users[0].id, {
    offset: 1,
    limit: 2,
    // TODO why is this needed? It does try to make a subquery otherwise, and then it doesn't work.
    // https://selleo.com/til/posts/ddesmudzmi-offset-pagination-with-subquery-in-sequelize-
    subQuery: false,
    include: [
      {
        model: User,
        as: 'Follows',
        include: [
          {
            model: Post,
          }
        ],
      },
    ],
  })).Follows
  assert(user0Follows[0].name === 'user1')
  assert(user0Follows[1].name === 'user2')
  assert(user0Follows.length === 2)
  const postsFound = []
  for (const followedUser of user0Follows) {
    postsFound.push(...followedUser.Posts)
  }
  postsFound.sort((x, y) => { return x.body < y.body ? -1 : x.body > y.body ? 1 : 0 })
  // Note that what happens is that some of the
  assert(postsFound[0].body === 'body11')
  assert(postsFound[1].body === 'body20')
  assert(postsFound.length === 2)

  // Same as above, but now with DESC ordering.
  {
    const user0Follows = (await User.findByPk(users[0].id, {
      order: [[
        {model: User, as: 'Follows'},
        Post,
        'body',
        'DESC'
      ]],
      offset: 1,
      limit: 2,
      subQuery: false,
      include: [
        {
          model: User,
          as: 'Follows',
          include: [
            {
              model: Post,
            }
          ],
        },
      ],
    })).Follows
    // Note how user ordering is also reversed from an ASC.
    // it likely takes the use that has the first post.
    assert(user0Follows[0].name === 'user2')
    assert(user0Follows[1].name === 'user1')
    assert(user0Follows.length === 2)
    const postsFound = []
    for (const followedUser of user0Follows) {
      postsFound.push(...followedUser.Posts)
    }
    // In this very specific data case, this would not be needed.
    // because user2 has the second post body and user1 has the first
    // alphabetically.
    postsFound.sort((x, y) => { return x.body < y.body ? 1 : x.body > y.body ? -1 : 0 })
    // Note that what happens is that some of the
    assert(postsFound[0].body === 'body20')
    assert(postsFound[1].body === 'body11')
    assert(postsFound.length === 2)
  }

  // Here user2 would have no post hits due to the limit,
  // so it is entirely pruned from the user list as desired.
  // Otherwise we would fetch a lot of unwanted user data
  // in a large database.
  const user0FollowsLimit2 = (await User.findByPk(users[0].id, {
    limit: 2,
    subQuery: false,
    include: [
      {
        model: User,
        as: 'Follows',
        include: [ { model: Post } ],
      },
    ],
  })).Follows
  assert(user0FollowsLimit2[0].name === 'user1')
  assert(user0FollowsLimit2.length === 1)

  // Case in which our post-sorting is needed.
  // TODO: possible to get sequelize to do this for us by returning
  // a flat array directly?
  // It's not big deal since the LIMITed result should be small,
  // but feels wasteful.
  // https://stackoverflow.com/questions/41502699/return-flat-object-from-sequelize-with-association
  // https://github.com/sequelize/sequelize/issues/4419
  {
    await Post.truncate({restartIdentity: true})
    const posts = await Post.bulkCreate([
      {body: 'body0', UserId: users[0].id},
      {body: 'body1', UserId: users[1].id},
      {body: 'body2', UserId: users[2].id},
      {body: 'body3', UserId: users[3].id},
      {body: 'body4', UserId: users[0].id},
      {body: 'body5', UserId: users[1].id},
      {body: 'body6', UserId: users[2].id},
      {body: 'body7', UserId: users[3].id},
    ])
    const user0Follows = (await User.findByPk(users[0].id, {
      order: [[
        {model: User, as: 'Follows'},
        Post,
        'body',
        'DESC'
      ]],
      subQuery: false,
      include: [
        {
          model: User,
          as: 'Follows',
          include: [
            {
              model: Post,
            }
          ],
        },
      ],
    })).Follows
    assert(user0Follows[0].name === 'user2')
    assert(user0Follows[1].name === 'user1')
    assert(user0Follows.length === 2)
    const postsFound = []
    for (const followedUser of user0Follows) {
      postsFound.push(...followedUser.Posts)
    }
    // We need this here, otherwise we would get all user2 posts first:
    // body6, body2, body5, body1
    postsFound.sort((x, y) => { return x.body < y.body ? 1 : x.body > y.body ? -1 : 0 })
    assert(postsFound[0].body === 'body6')
    assert(postsFound[1].body === 'body5')
    assert(postsFound[2].body === 'body2')
    assert(postsFound[3].body === 'body1')
    assert(postsFound.length === 4)
  }
}

await sequelize.close();
})();

Super many to many to do the "posts by followed users" query without post processing

Super many to many means explicitly setting belongsTo/hasMany between each model and the through table, in addition to the belongsToMany of each model.

This is the only way I found to nicely make the "posts by followed users" query without post processing.

const assert = require('assert');
const path = require('path');

const { Sequelize, DataTypes, Op } = require('sequelize');

const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: 'tmp.' + path.basename(__filename) + '.sqlite',
  define: {
    timestamps: false
  },
});

(async () => {

// Create the tables.
const User = sequelize.define('User', {
  name: { type: DataTypes.STRING },
});
const Post = sequelize.define('Post', {
  body: { type: DataTypes.STRING },
});
const UserFollowUser = sequelize.define('UserFollowUser', {
    UserId: {
      type: DataTypes.INTEGER,
      references: {
        model: User,
        key: 'id'
      }
    },
    FollowId: {
      type: DataTypes.INTEGER,
      references: {
        model: User,
        key: 'id'
      }
    },
  }
);

// Super many to many.
User.belongsToMany(User, {through: UserFollowUser, as: 'Follows'});
UserFollowUser.belongsTo(User)
User.hasMany(UserFollowUser)

User.hasMany(Post);
Post.belongsTo(User);

await sequelize.sync({force: true});

// Create data.
const users = await User.bulkCreate([
  {name: 'user0'},
  {name: 'user1'},
  {name: 'user2'},
  {name: 'user3'},
])
const posts = await Post.bulkCreate([
  {body: 'body0', UserId: users[0].id},
  {body: 'body1', UserId: users[1].id},
  {body: 'body2', UserId: users[2].id},
  {body: 'body3', UserId: users[3].id},
  {body: 'body4', UserId: users[0].id},
  {body: 'body5', UserId: users[1].id},
  {body: 'body6', UserId: users[2].id},
  {body: 'body7', UserId: users[3].id},
])
await users[0].addFollows([users[1], users[2]])

// Get all the posts by authors that user0 follows.
// without any post process sorting. We only managed to to this
// with a super many to many, because that allows us to specify
// a reversed order in the through table with `on`, since we need to
// match with `FollowId` and not `UserId`.
{
  const postsFound = await Post.findAll({
    order: [[
      'body',
      'DESC'
    ]],
    include: [
      {
        model: User,
        attributes: [],
        required: true,
        include: [
          {
            model: UserFollowUser,
            on: {
              FollowId: {[Op.col]: 'User.id' },
            },
            attributes: [],
            where: {UserId: users[0].id},
          }
        ],
      },
    ],
  })
  assert.strictEqual(postsFound[0].body, 'body6')
  assert.strictEqual(postsFound[1].body, 'body5')
  assert.strictEqual(postsFound[2].body, 'body2')
  assert.strictEqual(postsFound[3].body, 'body1')
  assert.strictEqual(postsFound.length, 4)
}

await sequelize.close();
})();
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985