Sequelize: multiple where clause - mysql

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:
Here is followers table:
And the Suscribes table:
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?

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.

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();
})();

Related

I'm trying to join 3 tables in node.js using sequelize but having trouble. What is the problem with my code?

Newer to Node.js any advice would be appreciated! Trying to join 3 tables with a common key but getting the error...
react_devtools_backend.js:4026 [GraphQL error]: Message: Unknown column 'google_responsive_descriptions.googleTextAdId' in 'on clause', Location: [object Object], Path: listGoogleTextAds
googleResponsiveAds.js file
module.exports = (sequelize, DataTypes) => {
const googleResponsiveAds = sequelize.define(
"google_responsive_headlines",
{
responsive_headlines: { type: DataTypes.STRING },
responsive_path_1: { type: DataTypes.STRING },
responsive_path_2: { type: DataTypes.STRING }
},
{
timestamps: false,
tableName: "google_responsive_headlines"
}
);
//googleResponsiveAds.associate = () => {};
googleResponsiveAds.associate = function (models) {
googleResponsiveAds.belongsTo(models.google_text_ads, { foreignKey: "ad_id" });
}
return googleResponsiveAds;
};
googleResponsiveDescriptionsAds.js file
module.exports = (sequelize, DataTypes) => {
const googleResponsiveDescriptionsAds = sequelize.define(
"google_responsive_descriptions",
{
responsive_descriptions: { type: DataTypes.STRING }
},
{
timestamps: false,
tableName: "google_responsive_descriptions"
}
);
//googleResponsiveDescriptionsAds.associate = () => {};
googleResponsiveDescriptionsAds.associate = function (models) {
googleResponsiveDescriptionsAds.belongsTo(models.google_text_ads, { foreignKey: "ad_id" });
}
return googleResponsiveDescriptionsAds;
};
googleTextAds.js file
module.exports = (sequelize, DataTypes) => {
const googleTextAds = sequelize.define(
"google_text_ads",
{
headline_pt_1: { type: DataTypes.STRING },
headline_pt_2: { type: DataTypes.STRING },
headline_pt_3: { type: DataTypes.STRING },
final_url: { type: DataTypes.STRING },
display_url: { type: DataTypes.STRING },
status: { type: DataTypes.STRING },
type: { type: DataTypes.STRING },
description1: { type: DataTypes.STRING },
description2: { type: DataTypes.STRING },
path1: { type: DataTypes.STRING },
path2: { type: DataTypes.STRING },
ad_id: { type: DataTypes.INTEGER }
},
{
timestamps: false,
tableName: "google_text_ads"
}
);
//googleTextAds.associate = () => {};
googleTextAds.associate = function (models) {
googleTextAds.hasMany(models.google_responsive_headlines, { sourceKey: 'ad_id' })
googleTextAds.hasMany(models.google_responsive_descriptions, { sourceKey: 'ad_id' });
};
return googleTextAds;
};
Here is the section of the queries/google.js
{
key: "listGoogleTextAds",
prototype:
"(customer_id: Int, start_date: String, end_date: String): [GoogleTextAds]",
run: async args => {
const allIds = await google_text_ads
.findAll({
attributes: [
"ad_id",
"date",
"impressions",
"clicks",
"cost"
],
include: [
{
model: google_responsive_descriptions,
as: 'google_responsive_descriptions',
required: true,
attributes: [
"ad_id",
"responsive_descriptions"
],
},
{
model: google_responsive_headlines,
as: 'google_responsive_headlines',
attributes: [
"ad_id",
"responsive_headlines",
"responsive_path_1",
"responsive_path_2"
]
}
],
where: {
customer_id: args.customer_id,
date: {
[Op.gte]: args.start_date,
[Op.lte]: args.end_date
},
status: {
[Op.in]: ["ENABLED"]
},
type: {
[Op.in]: ["EXPANDED_TEXT_AD", "RESPONSIVE_SEARCH_AD"]
}
}
})
}
}
EDIT: Here's my query
export const LIST_GOOGLE_TEXT_ADS = gql`
query listGoogleTextAds(
$customer_id: Int!
$start_date: String!
$end_date: String!
) {
listGoogleTextAds(
customer_id: $customer_id
start_date: $start_date
end_date: $end_date
) {
ad_id
type
headline_pt_1
headline_pt_2
headline_pt_3
description1
description2
path1
path2
status
final_url
display_url
impressions
clicks
cost
}
}
`;
and my models...
type GoogleTextAds {
ad_id: Int
type: String
headline_pt_1: String
headline_pt_2: String
headline_pt_3: String
description1: String
description2: String
path1: String
path2: String
final_url: String
display_url: String
status: String
impressions: Int
clicks: Int
cost: Float
}
type GoogleResponsiveAds{
ad_id: Int
responsive_headlines: String
responsive_path_1: String
responsive_path_2: String
}
type GoogleResponsiveDescriptionsAds{
ad_id: Int
responsive_descriptions: String
}
here is the query I am trying to replicate...
SELECT distinct google_responsive_headlines.responsive_headlines,
google_responsive_headlines.responsive_path_1,
google_responsive_headlines.responsive_path_2,
google_responsive_descriptions.responsive_descriptions,
google_text_ads.date,
google_text_ads.clicks,
google_text_ads.cost,
google_text_ads.impressions,
google_text_ads.ad_id,
google_text_ads.status,
google_text_ads.final_url,
google_text_ads.create_time
FROM irene_db.google_text_ads
inner JOIN irene_db.google_responsive_headlines ON google_responsive_headlines.ad_id = google_text_ads.ad_id
inner JOIN irene_db.google_responsive_descriptions ON google_responsive_descriptions.ad_id = google_text_ads.ad_id
where google_text_ads.customer_id = 144 and google_text_ads.date = '2022-06-09' and
google_responsive_headlines.customer_id = 144 and google_responsive_headlines.date = '2022-06-09' and
google_responsive_descriptions.customer_id = 144 and google_responsive_descriptions.date = '2022-06-09';
EDIT2: Where associate gets called...
const fs = require("fs");
const path = require("path");
const Sequelize = require("sequelize");
const dotenv = require("dotenv");
dotenv.config();
const basename = path.basename(module.filename);
const db = {};
let sequelize;
const { DB_HOST, DB_USER, DB_PASS, DB_NAME } = process.env;
sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASS, {
dialect: "mysql",
host: DB_HOST
});
fs.readdirSync(__dirname)
.filter(
file =>
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
)
.forEach(file => {
const model = sequelize.import(path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
EDIT 3:
After boc4life's code suggestions I'm now at least getting graphql to attempt the query. But it's using the wrong field name in the join on section. Here's the query it built...
SELECT
`google_text_ads`.`id`,
`google_text_ads`.`ad_id`,
`google_text_ads`.`date`,
`google_text_ads`.`impressions`,
`google_text_ads`.`clicks`,
`google_text_ads`.`cost`,
`google_responsive_descriptions`.`id` AS `google_responsive_descriptions.id`,
`google_responsive_descriptions`.`responsive_descriptions` AS `google_responsive_descriptions.responsive_descriptions`,
`google_responsive_headlines`.`id` AS `google_responsive_headlines.id`,
`google_responsive_headlines`.`responsive_headlines` AS `google_responsive_headlines.responsive_headlines`,
`google_responsive_headlines`.`responsive_path_1` AS `google_responsive_headlines.responsive_path_1`,
`google_responsive_headlines`.`responsive_path_2` AS `google_responsive_headlines.responsive_path_2`
FROM
`google_text_ads` AS `google_text_ads`
INNER JOIN
`google_responsive_descriptions` AS `google_responsive_descriptions` ON `google_text_ads`.`ad_id` = `google_responsive_descriptions`.`googleTextAdId`
INNER JOIN
`google_responsive_headlines` AS `google_responsive_headlines` ON `google_text_ads`.`ad_id` = `google_responsive_headlines`.`googleTextAdId`
WHERE
`google_text_ads`.`customer_id` = 142
AND (`google_text_ads`.`date` >= '2022-05-17'
AND `google_text_ads`.`date` <= '2022-06-17')
AND `google_text_ads`.`status` IN ('ENABLED')
AND `google_text_ads`.`type` IN ('EXPANDED_TEXT_AD' , 'RESPONSIVE_SEARCH_AD');
Try using the tableName defined while defining the table structure to access the model while creating associations.
Example: instead of
models.googleTextAds
do
models.google_text_ads
What jumps out to me is all of those nested includes. Since you are joining the Headlines and Descriptions tables on the Text_Ads table, I believe all you should need here is the one include array containing two objects, one for Headlines and one for Descriptions.
Currently you have Headlines nested under Descriptions, which won't work because Descriptions does not have an association with Headlines directly defined. You also have an include of Text_Ads nested inside of Descriptions, which should WORK, but should be unnecessary since that is the model you are calling findAll() on. You can bring the Text_Ads attributes you are querying for out into that parent object as a sibling of include. Something like this looks like a good starting point for getting the query cleaned up. I have also removed a bunch of the unnecessary sequelize.col() that you had in your initial post.
const allIds = await google_text_ads
.findAll({
attributes: [
"ad_id",
[sequelize.fn("max", sequelize.col("date")), "date"],
[sequelize.fn("sum", sequelize.col("impressions")), "impressions"],
[sequelize.fn("sum", sequelize.col("clicks")), "clicks"],
[sequelize.fn("sum", sequelize.col("cost")), "cost"]
],
include: [
{
model: Models.google_responsive_descriptions,
as: 'googleResponsiveDescriptionsAds',
required: true,
attributes: [
"ad_id",
"responsive_descriptions"
],
},
{
model: Models.google_responsive_headlines,
as: 'googleResponsiveAds',
attributes: [
"ad_id",
"responsive_headlines",
"responsive_path_1",
"responsive_path_2"
]
}
],
where: {
customer_id: args.customer_id,
date: {
[Op.gte]: args.start_date,
[Op.lte]: args.end_date
},
status: {
[Op.in]: ["ENABLED"]
},
type: {
[Op.in]: ["EXPANDED_TEXT_AD", "RESPONSIVE_SEARCH_AD"]
}
}
})

Sequilize query is returning only one row while using include

Context : I am having this problem were I am doing a query using sequilize an it only return's me an array with one position even though I have more than one field that correspond to the query.
This are my two involved models
This is my group.js model
module.exports = (sequelize, DataTypes) => {
const Group = sequelize.define('Group', {
name: DataTypes.STRING,
limit: DataTypes.STRING,
user_id: DataTypes.INTEGER
});
Group.associate = models => {
Group.belongsTo(models.User, { foreignKey: 'user_id' });
};
Group.associate = models => {
Group.hasMany(models.Movement, { foreignKey: 'group_id' });
};
return Group;
}
This is my movement.js model
module.exports = (sequelize, DataTypes) => {
const Mov = sequelize.define('Movement', {
description: DataTypes.STRING,
value: DataTypes.INTEGER,
group_id: DataTypes.INTEGER
});
Mov.associate = models => {
Mov.hasOne(models.Group, { foreignKey: 'group_id' });
};
return Mov;
}
This is my query (where you will see that I am doing an INNER JOIN to SUM the fields of the Movement table)
router.get('/', verify, async (req, res) => {
try {
const group = await Group.findAll({
attributes: [
'id',
'name',
'limit',
[sequelize.fn('SUM', sequelize.col('Movements.value')), 'total_spent'],
],
include: [{
attributes: [], // this is empty because I want to hide the Movement object in this query (if I want to show the object just remove this)
model: Movement,
required: true
}],
where: {
user_id: req.userId
}
});
if (group.length === 0) return res.status(400).json({ error: "This user has no groups" })
res.status(200).json({ groups: group }) //TODO see why this is onyl return one row
} catch (error) {
console.log(error)
res.status(400).json({ Error: "Error while fetching the groups" });
}
});
Problem is that it only return's one position of the expected array :
{
"groups": [
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
}
]
}
It should return 2 positions
{
"groups": [
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
},
{
"id": 9,
"name": "rgrgrg",
"limit": 3454354,
"total_spent": "2533"
}
]
}
This is the query sequilize is giving me:
SELECT `Group`.`id`, `Group`.`name`, `Group`.`limit`, SUM(`Movements`.`value`) AS `total_spent` FROM `Groups` AS `Group` INNER JOIN `Movements` AS `Movements` ON `Group`.`id` = `Movements`.`group_id` WHERE `Group`.`user_id` = 1;
I guess you need to add an appropriate group by clause as follows -
const group = await Group.findAll({
attributes: [
'id',
'name',
'limit',
[sequelize.fn('SUM', sequelize.col('Movements.value')), 'total_spent'],
],
include: [{
attributes: [], // this is empty because I want to hide the Movement object in this query (if I want to show the object just remove this)
model: Movement,
required: true
}],
where: {
user_id: req.userId
},
group: '`Movements`.`group_id`'
});
Many-to-many "through" table with multiple rows of identical foreign key pairs only returns one result?
I just ran into this bug and added this options to the main query:
{
raw: true,
plain: false,
nest: true
}
Then you just merge the query.
It's a workaround, but might help someone.

How to use findAll with associations in Sequelize

I'm having problems to use the findAll() method with associations from Sequelize.
I have two models: Posts and Authors (an author has many posts and one post has one author), that I have created with Sequelize-cli and then through the migration command npx sequelize db migrate:all i have created them in mysql. To keep things organized, I have the associations between the models in another migration file (created with npx sequelize init:migrations, after all the models already existent), so my code looks like this:
AUTHOR MODEL
'use strict';
module.exports = (sequelize, DataTypes) => {
const Author = sequelize.define('Author', {
authorName: {
type: DataTypes.STRING,
validate: {
is: ["^[a-z]+$",'i'],
}
},
biography: {
type: DataTypes.TEXT,
validate: {
notEmpty: true,
}
}
}, {});
Author.associate = function(models) {
Author.hasMany(models.Post);
};
return Author;
};
POST MODEL
'use strict';
module.exports = (sequelize, DataTypes) => {
const Post = sequelize.define('Post', {
title: {
type: DataTypes.STRING,
validate: {
is: ["^[a-z]+$",'i'],
notEmpty: true,
},
},
content: {
type: DataTypes.TEXT,
validate: {
notEmpty: true,
},
},
likes: {
type: DataTypes.INTEGER,
defaultValue: 0,
validate: {
isInt: true,
},
},
}, {});
Post.associate = function(models) {
// associations can be defined here
};
return Post;
};
ASSOCIATIONS FILE (MIGRATION) (showing only parts that matter)
up: (queryInterface, Sequelize) => {
return queryInterface.sequelize.transaction(t => {
return Promise.all([
queryInterface.addColumn('Posts','AuthorId', {
type: Sequelize.INTEGER,
references: {
model: 'Authors',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
}, { transaction: t }),
queryInterface.addColumn('Posts', 'ImagesId', {
type: Sequelize.INTEGER,
references: {
model: 'Images',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
}, { transaction: t }),
queryInterface.addColumn('Posts', 'CategoryId', {
type: Sequelize.INTEGER,
references: {
model: 'Categories',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL',
}, { transaction: t }),
]);
});
This is working fine apparently, since in Mysql-Workbench it shows me the following:
But, when I try to use the findAll() like this:
const { Post, Author } = require('../models/index');
function(response) {
Post.findAll({
attributes: ['id', 'title', 'content', 'likes'],
include: {
model: Author,
}
})
.then(result => response.json(result))
.catch(error => response.send(`Error getting data. Error: ${error}`));
It gives me the following error:
SequelizeEagerLoadingError: Author is not associated to Post!
So, I dont know anymore how to proceed. I've been trying many others approaches, but all of then unsuccessfully. I read already many other questions here in StackOverFlow about how to solve this sort of problem, but those were unsuccessfully too.
Thanks in advance.
You need to define the association for Post also as you are querying upon Post model
Post.associate = function(models) {
Post.belongsTo((models.Author);
};
You need to add an association from both ends, Post -> Author and Author -> Post , this way you will never stuck in this kind of error.
Summarizing this documentation we have the following:
If you have these models:
const User = sequelize.define('user', { name: DataTypes.STRING });
const Task = sequelize.define('task', { name: DataTypes.STRING });
And they are associated like this:
User.hasMany(Task);
Task.belongsTo(User);
You can fetch them with its associated elements in these ways:
const tasks = await Task.findAll({ include: User });
Output:
[{
"name": "A Task",
"id": 1,
"userId": 1,
"user": {
"name": "John Doe",
"id": 1
}
}]
And
const users = await User.findAll({ include: Task });
Output:
[{
"name": "John Doe",
"id": 1,
"tasks": [{
"name": "A Task",
"id": 1,
"userId": 1
}]
}]

Sequelize include even if it's null

I am using Sequelize express with Node.js as the backend, some data from my sequelize I need to include to another table but some of these data is null so the whole result I’m getting is null.
Question: how can I return some data if data it's available and return the other null if not data is there
router.get("/scheduled/:id", function(req, res, next) {
models.Order.findOne({
where: {
id: req.params.id
},
attributes: ['orderStatus', 'id', 'serviceId', 'orderDescription', 'orderScheduledDate'],
include: [{
model: models.User,
attributes: ['firstName', 'phoneNumber']
}]
}).then(function(data) {
res.status(200).send({
data: data,
serviceName: data["serviceId"]
});
});
});
I want: the result should return null if there is no user for the order and return order details and user when it is null.
However, a where clause on a related model will create an inner join and return only the instances that have matching sub-models. To return all parent instances, you should add required: false for more detail check nested-eager-loading
var users = require('./database/models').user;
models.Order.findOne({
where: {
id: req.params.id
},attributes: ['orderStatus','id','serviceId','orderDescription','orderScheduledDate'],
include: [
{model: users,required: false,
attributes: ['firstName','phoneNumber']
}
]
}).then(function(data) {
res.status(200).send({data : data,serviceName : data["serviceId"]});
});
You can add attribute required: false,
const result = await company.findAndCountAll({
where: conditions,
distinct: true,
include: [
media,
{
model: tag,
where: tagCond,
},
{ model: users, where: userCond, attributes: ['id'] },
{
model: category_company,
as: 'categoryCompany',
where: categoryCond,
},
{ model: media, as: 'logoInfo' },
{ model: city, as: 'city' },
{
model: employee,
as: 'employees',
required: false,
include: [{
model: media,
as: 'avatarInfo',
}],
where: {
publish: {
[Op.ne]: -1,
},
},
},
],
order: [['createdAt', 'DESC']],
...paginate({ currentPage: page, pageSize: limit }),
});

Counting associated entries with Sequelize

I have two tables, locations and sensors. Each entry in sensors has a foreign key pointing to locations. Using Sequelize, how do I get all entries from locations and total count of entries in sensors that are associated with each entry in locations?
Raw SQL:
SELECT
`locations`.*,
COUNT(`sensors`.`id`) AS `sensorCount`
FROM `locations`
JOIN `sensors` ON `sensors`.`location`=`locations`.`id`;
GROUP BY `locations`.`id`;
Models:
module.exports = function(sequelize, DataTypes) {
var Location = sequelize.define("Location", {
id: {
type: DataTypes.INTEGER.UNSIGNED,
primaryKey: true
},
name: DataTypes.STRING(255)
}, {
classMethods: {
associate: function(models) {
Location.hasMany(models.Sensor, {
foreignKey: "location"
});
}
}
});
return Location;
};
module.exports = function(sequelize, DataTypes) {
var Sensor = sequelize.define("Sensor", {
id: {
type: DataTypes.INTEGER.UNSIGNED,
primaryKey: true
},
name: DataTypes.STRING(255),
type: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: "sensor_types",
key: "id"
}
},
location: {
type: DataTypes.INTEGER.UNSIGNED,
references: {
model: "locations",
key: "id"
}
}
}, {
classMethods: {
associate: function(models) {
Sensor.belongsTo(models.Location, {
foreignKey: "location"
});
Sensor.belongsTo(models.SensorType, {
foreignKey: "type"
});
}
}
});
return Sensor;
};
Use findAll() with include() and sequelize.fn() for the COUNT:
Location.findAll({
attributes: {
include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]]
},
include: [{
model: Sensor, attributes: []
}]
});
Or, you may need to add a group as well:
Location.findAll({
attributes: {
include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]]
},
include: [{
model: Sensor, attributes: []
}],
group: ['Location.id']
})
For Counting associated entries with Sequelize
Location.findAll({
attributes: {
include: [[Sequelize.fn('COUNT', Sequelize.col('sensors.location')), 'sensorCounts']]
}, // Sequelize.col() should contain a attribute which is referenced with parent table and whose rows needs to be counted
include: [{
model: Sensor, attributes: []
}],
group: ['sensors.location'] // groupBy is necessary else it will generate only 1 record with all rows count
})
Note :
Some how, this query generates a error like sensors.location is not exists in field list. This occur because of subQuery which is formed by above sequelize query.
So solution for this is to provide subQuery: false like example
Location.findAll({
subQuery: false,
attributes: {
include: [[Sequelize.fn('COUNT', Sequelize.col('sensors.location')), 'sensorCounts']]
},
include: [{
model: Sensor, attributes: []
}],
group: ['sensors.location']
})
Note:
**Sometime this could also generate a error bcz of mysql configuration which by default contains only-full-group-by in sqlMode, which needs to be removed for proper working.
The error will look like this..**
Error : Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'db.table.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
So to resolve this error follow this answer
SELECT list is not in GROUP BY clause and contains nonaggregated column .... incompatible with sql_mode=only_full_group_by
Now this will successfully generate all associated counts
Hope this will help you or somebody else!
Location.findAll({
attributes: {
include: [[Sequelize.fn("COUNT", Sequelize.col("sensors.id")), "sensorCount"]]
},
include: [{
model: Sensor, attributes: []
}]
});
and it works. but when i add "limit", i got error: sensors undefined
Example of HAVING, ORDER BY, INNER vs OUTER JOIN + several bugs/unintuitive behavior
I went into more detail at: Sequelize query with count in inner join but here's a quick summary list of points:
you must use row.get('count'), row.count does not work
you must parseInt on PostgreSQL
this code fails on PostgreSQL with column X must appear in the GROUP BY clause or be used in an aggregate function due to a sequelize bug
OUTER JOIN example which includes 0 counts by using required: false:
sqlite.js
const assert = require('assert');
const { DataTypes, Op, Sequelize } = require('sequelize');
const sequelize = new Sequelize('tmp', undefined, undefined, Object.assign({
dialect: 'sqlite',
storage: 'tmp.sqlite'
}));
;(async () => {
const User = sequelize.define('User', {
name: { type: DataTypes.STRING },
}, {});
const Post = sequelize.define('Post', {
body: { type: DataTypes.STRING },
}, {});
User.belongsToMany(Post, {through: 'UserLikesPost'});
Post.belongsToMany(User, {through: 'UserLikesPost'});
await sequelize.sync({force: true});
const user0 = await User.create({name: 'user0'})
const user1 = await User.create({name: 'user1'})
const user2 = await User.create({name: 'user2'})
const post0 = await Post.create({body: 'post0'})
const post1 = await Post.create({body: 'post1'})
const post2 = await Post.create({body: 'post2'})
// Set likes for each user.
await user0.addPosts([post0, post1])
await user1.addPosts([post0, post2])
let rows = await User.findAll({
attributes: [
'name',
[sequelize.fn('COUNT', sequelize.col('Posts.id')), 'count'],
],
include: [
{
model: Post,
attributes: [],
required: false,
through: {attributes: []},
where: { id: { [Op.ne]: post2.id }},
},
],
group: ['User.name'],
order: [[sequelize.col('count'), 'DESC']],
having: sequelize.where(sequelize.fn('COUNT', sequelize.col('Posts.id')), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows[1].name, 'user2')
assert.strictEqual(parseInt(rows[1].get('count'), 10), 0)
assert.strictEqual(rows.length, 2)
})().finally(() => { return sequelize.close() });
with:
package.json
{
"name": "tmp",
"private": true,
"version": "1.0.0",
"dependencies": {
"pg": "8.5.1",
"pg-hstore": "2.3.3",
"sequelize": "6.5.1",
"sqlite3": "5.0.2"
}
}
and Node v14.17.0.
INNER JOIN version excluding 0 counts:
let rows = await User.findAll({
attributes: [
'name',
[sequelize.fn('COUNT', '*'), 'count'],
],
include: [
{
model: Post,
attributes: [],
through: {attributes: []},
where: { id: { [Op.ne]: post2.id }},
},
],
group: ['User.name'],
order: [[sequelize.col('count'), 'DESC']],
having: sequelize.where(sequelize.fn('COUNT', '*'), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows.length, 1)
How about defining a database view for it and then a model for that view? You can just get the relationship to the view included in your query whenever you need the number of sensors. The code may look cleaner this way, but I'm not aware if there will be performance costs. Somebody else may answer that...
CREATE OR REPLACE VIEW view_location_sensors_count AS
select "locations".id as "locationId", count("sensors".id) as "locationSensorsCount"
from locations
left outer join sensors on sensors."locationId" = location.id
group by location.id
When defining the model for the view you remove the id attribute and set the locationId as the primary key.
Your model could look like this:
const { Model, DataTypes } = require('sequelize')
const attributes = {
locationID: {
type: DataTypes.UUIDV4, // Or whatever data type is your location ID
primaryKey: true,
unique: true
},
locationSensorsCount: DataTypes.INTEGER
}
const options = {
paranoid: false,
modelName: 'ViewLocationSensorsCount',
tableName: 'view_location_sensors_count',
timestamps: false
}
/**
* This is only a database view. It is not an actual table, so
* DO NOT ATTEMPT insert, update or delete statements on this model
*/
class ViewLocationSensorsCount extends Model {
static associate(models) {
ViewLocationSensorsCount.removeAttribute('id')
ViewLocationSensorsCount.belongsTo(models.Location, { as:'location', foreignKey: 'locationID' })
}
static init(sequelize) {
this.sequelize = sequelize
return super.init(attributes, {...options, sequelize})
}
}
module.exports = ViewLocationSensorsCount
In the end, in your Location model you set a hasOne relationship to the Sensor model.