Sequelize find based on association - node.js

How would I use Sequelize to find all people where a column in the relation satisfies a condition?
An example would be to find all Books whose author's last name is 'Hitchcock'. The book schema contains a hasOne relation with the Author's table.
Edit: I understand how this could be done with a raw SQL query, but looking for another approach

Here's a working sample of how to user Sequelize to get all Books by an Author with a certain last name. It looks quite a bit more complicated than it is, because I am defining the Models, associating them, syncing with the database (to create their tables), and then creating dummy data in those new tables. Look for the findAll in the middle of the code to see specifically what you're after.
module.exports = function(sequelize, DataTypes) {
var Author = sequelize.define('Author', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
firstName: {
type: DataTypes.STRING
},
lastName: {
type: DataTypes.STRING
}
})
var Book = sequelize.define('Book', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
title: {
type: DataTypes.STRING
}
})
var firstAuthor;
var secondAuthor;
Author.hasMany(Book)
Book.belongsTo(Author)
Author.sync({ force: true })
.then(function() {
return Book.sync({ force: true });
})
.then(function() {
return Author.create({firstName: 'Test', lastName: 'Testerson'});
})
.then(function(author1) {
firstAuthor=author1;
return Author.create({firstName: 'The Invisible', lastName: 'Hand'});
})
.then(function(author2) {
secondAuthor=author2
return Book.create({AuthorId: firstAuthor.id, title: 'A simple book'});
})
.then(function() {
return Book.create({AuthorId: firstAuthor.id, title: 'Another book'});
})
.then(function() {
return Book.create({AuthorId: secondAuthor.id, title: 'Some other book'});
})
.then(function() {
// This is the part you're after.
return Book.findAll({
where: {
'Authors.lastName': 'Testerson'
},
include: [
{model: Author, as: Author.tableName}
]
});
})
.then(function(books) {
console.log('There are ' + books.length + ' books by Test Testerson')
});
}

In the newest version of Sequilize (5.9.0) the method proposed by #c.hill does not work.
Now you need to do the following:
return Book.findAll({
where: {
'$Authors.lastName$': 'Testerson'
},
include: [
{model: Author, as: Author.tableName}
]
});

For documentation!
Check the eager loading section
https://sequelize.org/master/manual/eager-loading.html
For the above answers! You can find it in the doc at the following title
Complex where clauses at the top-level
From the doc:
To obtain top-level WHERE clauses that involve nested columns, Sequelize provides a way to reference nested columns: the '$nested.column$' syntax.
It can be used, for example, to move the where conditions from an included model from the ON condition to a top-level WHERE clause.
User.findAll({
where: {
'$Instruments.size$': { [Op.ne]: 'small' }
},
include: [{
model: Tool,
as: 'Instruments'
}]
});
Generated SQL:
SELECT
`user`.`id`,
`user`.`name`,
`Instruments`.`id` AS `Instruments.id`,
`Instruments`.`name` AS `Instruments.name`,
`Instruments`.`size` AS `Instruments.size`,
`Instruments`.`userId` AS `Instruments.userId`
FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';
For a better understanding of all differences between the inner where option (used inside an include), with and without the required option, and a top-level where using the $nested.column$ syntax, below we have four examples for you:
// Inner where, with default `required: true`
await User.findAll({
include: {
model: Tool,
as: 'Instruments',
where: {
size: { [Op.ne]: 'small' }
}
}
});
// Inner where, `required: false`
await User.findAll({
include: {
model: Tool,
as: 'Instruments',
where: {
size: { [Op.ne]: 'small' }
},
required: false
}
});
// Top-level where, with default `required: false`
await User.findAll({
where: {
'$Instruments.size$': { [Op.ne]: 'small' }
},
include: {
model: Tool,
as: 'Instruments'
}
});
// Top-level where, `required: true`
await User.findAll({
where: {
'$Instruments.size$': { [Op.ne]: 'small' }
},
include: {
model: Tool,
as: 'Instruments',
required: true
}
});
Generated SQLs, in order:
-- Inner where, with default `required: true`
SELECT [...] FROM `users` AS `user`
INNER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
AND `Instruments`.`size` != 'small';
-- Inner where, `required: false`
SELECT [...] FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
AND `Instruments`.`size` != 'small';
-- Top-level where, with default `required: false`
SELECT [...] FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';
-- Top-level where, `required: true`
SELECT [...] FROM `users` AS `user`
INNER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';
And that give us a good look how the join's are done!

Related

exclude through attributes in sequelize

I have 2 tables post and tags. I'm using Tag to get all the posts associated with it.
models.Tag.findAll({
attributes: ['tagName'],
include: [
{model: models.Post
attributes: ['content']
through: {
attributes: []
}
}
]
})
The problem is that it selects all the through table attributes in the query.
Although doing include.through.attributes = [] the attributes don't show up in the result query but when I console.log the select query, it's still selecting all the attributes of the through table.
Is there to exclude the through table? it makes groupBy impossible in Postgres, cuz its selecting all the columns automatically.
I don't reproduce on sequelize#6.5.1 sqlite3#5.0.2 with:
#!/usr/bin/env node
// Find all posts by users that a given user follows.
// https://stackoverflow.com/questions/42632943/sequelize-multiple-where-clause
const assert = require('assert');
const path = require('path');
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'tmp.' + path.basename(__filename) + '.sqlite',
});
(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: 'body01', 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]])
const user0Follows = await User.findByPk(users[0].id, {
attributes: [
[Sequelize.fn('COUNT', Sequelize.col('Follows.Posts.id')), 'count']
],
include: [
{
model: User,
as: 'Follows',
attributes: [],
//through: { attributes: [] },
include: [
{
model: Post,
attributes: [],
}
],
},
],
})
assert.strictEqual(user0Follows.dataValues.count, 4);
await sequelize.close();
})();
The prettified generated SELECT is:
SELECT
`User`.`id`,
COUNT(`Follows->Posts`.`id`) AS `count`
FROM
`Users` AS `User`
LEFT OUTER JOIN `UserFollowUser` AS `Follows->UserFollowUser` ON `User`.`id` = `Follows->UserFollowUser`.`UserId`
LEFT OUTER JOIN `Users` AS `Follows` ON `Follows`.`id` = `Follows->UserFollowUser`.`FollowId`
LEFT OUTER JOIN `Posts` AS `Follows->Posts` ON `Follows`.`id` = `Follows->Posts`.`UserId`
WHERE
`User`.`id` = 1;
If I remove the through: { attributes: [] }, then the through attributes appear, so the statement is doing something as expected:
SELECT
`User`.`id`,
COUNT(`Follows->Posts`.`id`) AS `count`,
`Follows->UserFollowUser`.`createdAt` AS `Follows.UserFollowUser.createdAt`,
`Follows->UserFollowUser`.`updatedAt` AS `Follows.UserFollowUser.updatedAt`,
`Follows->UserFollowUser`.`UserId` AS `Follows.UserFollowUser.UserId`,
`Follows->UserFollowUser`.`FollowId` AS `Follows.UserFollowUser.FollowId`
FROM
`Users` AS `User`
LEFT OUTER JOIN `UserFollowUser` AS `Follows->UserFollowUser` ON `User`.`id` = `Follows->UserFollowUser`.`UserId`
LEFT OUTER JOIN `Users` AS `Follows` ON `Follows`.`id` = `Follows->UserFollowUser`.`FollowId`
LEFT OUTER JOIN `Posts` AS `Follows->Posts` ON `Follows`.`id` = `Follows->Posts`.`UserId`
WHERE
`User`.`id` = 1;
so likely this was fixed.

Sequelize: how to implement a search based on associated keywords?

I am looking to return all articles from the database associated with one or more keywords, but I am not sure there right way to go about this?
I am using Sequelize 3.x, with node.js 3.7.0.
The data model looks at follows:
const Article = sequelize.define('article', {
...
}
const Keyword = sequelize.define('keyword', {
name: Sequelize.STRING,
lang: Sequelize.STRING
}
const ArticleKeyword = sequelize.define('article_keyword', {
articleId: Sequelize.INTEGER,
keywordId: Sequelize.INTEGER
}
(Article).belongsToMany(
Keyword, { through: ArticleKeyword, as: 'keyword' });
(Keyword).belongsToMany(
Article { through: ArticleKeyword, as: 'article' });
Then the query I tried:
var keywordFilter;
if (req.body.keywords) {
var keywords = req.body.keywords);
if (typeof keywords === 'string') {
keywords = keywords.split(/ *, */);
}
keywordFilter = { name: { $in: keywords } };
}
Article.findAll({
where: {
deleted: false
},
include: [{
model: Keyword,
as: 'keywords',
where: keywordFilter,
attributes: ['name'],
through: {
attributes: []
}
}]
}).then(function(articles) {
...
});
The issue I am finding here is rather than selecting just the articles with the matching keywords it returns all the articles and then simply selects the keywords specified in the query for the results.
Can anyone suggest the right way to go about this?
Hi can you try passing
require:true
in the include block for inner join and check
Ref : https://stackoverflow.com/a/31680398/4583460

Sequelize - scope with include

I'm using Sequelize in my Node.js/Express app with the notion of scopes (http://docs.sequelizejs.com/en/latest/docs/scopes/).
I would like to have two scopes on a Model. Each of these scope are based on a join of another table (see the two scopes inLocation & withExpertises):
const Agency = sequelize.define("agency", {
id: { type: DataTypes.INTEGER, primaryKey: true },
name: DataTypes.STRING,
address: DataTypes.STRING,
size: DataTypes.INTEGER,
description: DataTypes.TEXT,
logoFileName: { type: DataTypes.STRING, field: "logo_file_name" },
externalScore: { type: DataTypes.FLOAT, field: "external_score" }
}, {
classMethods: {
associate(models) {
Agency.belongsToMany(models.location, {
through: "locationsAgency",
foreignKey: "location_id"
});
Agency.hasOne(models.locationsAgency);
Agency.hasMany(models.service);
}
},
scopes: {
inLocation(locationId) {
return {
include: [{
model: locationsAgency, where: { locationId: locationId }
}]
};
},
withExpertises(expertiseIds) {
return {
include: [{
model: service, where: { expertiseId: expertiseIds }
}]
};
},
orderByRelevance: {
order: '"externalScore" DESC'
}
}
});
When I'm doing the following call:
models.agency.scope({ method: ["inLocation", 9260] },
{ method: ["withExpertises", 79] },
"orderByRelevance")
.findAll({ limit: 10 })
I have this SQL generated:
SELECT "agency".*,
"services"."id" AS "services.id",
"services"."expertise_id" AS "services.expertiseId"
FROM (
SELECT "agency".*,
"locationsAgency"."id" AS "locationsAgency.id",
"locationsAgency"."location_id" AS "locationsAgency.locationId",
"locationsAgency"."agency_id" AS "locationsAgency.agencyId"
FROM "agencies" AS "agency"
INNER JOIN "locations_agencies" AS "locationsAgency" ON "agency"."id" = "locationsAgency"."agency_id"
AND "locationsAgency"."location_id" = 9260
WHERE (
SELECT "agency_id"
FROM "services" AS "service"
WHERE ("service"."agency_id" = "agency"."id" AND "service"."expertise_id" = 79)
LIMIT 1
)
IS NOT NULL LIMIT 10
) AS "agency"
INNER JOIN "services" AS "services" ON "agency"."id" = "services"."agency_id"
AND "services"."expertise_id" = 79
ORDER BY "externalScore" DESC
As you can see, the scope are nested in the FROM-clause, which give me a awful SQL and then the limit statement is not a the good place.
I expected to have this following SQL query:
SELECT "agency".*,
"services"."id" AS "services.id",
"services"."expertise_id" AS "services.expertiseId",
"services"."created_at" AS "services.created_at",
"services"."updated_at" AS "services.updated_at",
"locationsAgency"."id" AS "locationsAgency.id",
"locationsAgency"."location_id" AS "locationsAgency.locationId",
"locationsAgency"."agency_id" AS "locationsAgency.agencyId"
FROM "agencies" AS "agency"
INNER JOIN "locations_agencies" AS "locationsAgency" ON "agency"."id" = "locationsAgency"."agency_id"
INNER JOIN "services" AS "services" ON "agency"."id" = "services"."agency_id"
AND "locationsAgency"."location_id" = 9260
AND "services"."expertise_id" = 79
ORDER BY "external_score" DESC
LIMIT 10
Any ideas how to have this sql with the use of scopes?
Thanks!

Sequelize find by association through manually-defined join table

I know that there is a simpler case described here:
Unfortunately, my case is a bit more complex than that. I have a User model which belongsToMany Departments (which in turn belongsToMany Users), but does so through userDepartment, a manually defined join table. My goal is to get all the users belonging to a given department. First let's look at models/user.js:
var user = sequelize.define("user", {
id: {
type: DataTypes.INTEGER,
field: 'emplId',
primaryKey: true,
autoIncrement: false
},
firstname: {
type: DataTypes.STRING,
field: 'firstname_preferred',
defaultValue: '',
allowNull: false
}
...
...
...
associate: function(models) {
user.belongsToMany(models.department, {
foreignKey: "emplId",
through: 'userDepartment'
});
})
}
...
return user;
Now, a look at models/department.js:
var department = sequelize.define("department", {
id: {
type: DataTypes.INTEGER,
field: 'departmentId',
primaryKey: true,
autoIncrement: true
},
...
classMethods: {
associate: function(models) {
department.belongsToMany(models.user, {
foreignKey: "departmentId",
through: 'userDepartment',
onDelete: 'cascade'
});
}
...
return department;
And finally at models/userDepartment.js:
var userDepartment = sequelize.define("userDepartment", {
title: {
type: DataTypes.STRING,
field: 'title',
allowNull: false,
defaultValue: ''
}
}, {
tableName: 'user_departments'
});
return userDepartment;
So far so good. However, this query:
models.user.findAll({
where: {'departments.id': req.params.id},
include: [{model: models.department, as: models.department.tableName}]
})
Fails with the following error:
SequelizeDatabaseError: ER_BAD_FIELD_ERROR: Unknown column 'user.departments.id' in 'where clause'
Attempting to include userDepartment model results in:
Error: userDepartment (user_departments) is not associated to user!
In short: I have two Sequelize Models with a M:M relationship. They are associated through a manually defined join table (which adds a job title to each unique relationship, i.e., User A is a "Manager" in Department B). Attempting to find Users by Department fails with a bad table name error.
sequelize version "^2.0.5"
Took a couple of hours, but I found my solution:
models.department.find({
where: {id:req.params.id},
include: [models.user]
The problem is that Sequelize won't let you "go out of scope" because it begins each where clause with model_name. So, for example, the where clause was trying to compare user.departments.id when the departments table is only joined as departments.id. Since we're querying on a value of the department (the ID), it makes the most since to query for a single department and return their associated users.
I had a similair problem, but in my case I couldn't switch the tables.
I had to make use of the: sequelize.literal function.
In your case it would look like the following:
models.user.findAll({
where: sequelize.literal("departments.id = " + req.params.id),
include: [{model: models.department, as: models.department.tableName}]
})
I'm not fond of it, but it works.
For anyone still looking for an answer for this, I found one here on Github.
You simply do this:
where: {
'$Table.column$' : value
}
You can also use the auto-generated instance.getOthers() method if you have a class instance
This does potentially mean one extra query. But if the instance is already at hand, this is the most convenient syntax.
Supposing a "user likes post with given score" situation, we can get all the posts that a user likes with:
const user0 = await User.create({name: 'user0'})
const user0Likes = await user0.getPosts({order: [['body', 'ASC']]})
assert(user0Likes[0].body === 'post0');
assert(user0Likes[0].UserLikesPost.score === 1);
assert(user0Likes.length === 1);
Full runnable example:
main.js
const assert = require('assert')
const { DataTypes, Op, Sequelize } = require('sequelize')
const common = require('./common')
const sequelize = common.sequelize(__filename, process.argv[2], { 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 UserLikesPost = sequelize.define('UserLikesPost', {
UserId: {
type: DataTypes.INTEGER,
references: {
model: User,
key: 'id'
}
},
PostId: {
type: DataTypes.INTEGER,
references: {
model: Post,
key: 'id'
}
},
score: {
type: DataTypes.INTEGER,
},
});
User.belongsToMany(Post, {through: UserLikesPost});
Post.belongsToMany(User, {through: UserLikesPost});
await sequelize.sync({force: true});
// Create some users and likes.
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'});
// Autogenerated add* methods
// Make some useres like some posts.
await user0.addPost(post0, {through: {score: 1}})
await user1.addPost(post1, {through: {score: 2}})
await user1.addPost(post2, {through: {score: 3}})
// Find what user0 likes.
const user0Likes = await user0.getPosts({order: [['body', 'ASC']]})
assert(user0Likes[0].body === 'post0');
assert(user0Likes[0].UserLikesPost.score === 1);
assert(user0Likes.length === 1);
// Find what user1 likes.
const user1Likes = await user1.getPosts({order: [['body', 'ASC']]})
assert(user1Likes[0].body === 'post1');
assert(user1Likes[0].UserLikesPost.score === 2);
assert(user1Likes[1].body === 'post2');
assert(user1Likes[1].UserLikesPost.score === 3);
assert(user1Likes.length === 2);
// Where on the custom through table column.
// Find posts that user1 likes which have score greater than 2.
// https://stackoverflow.com/questions/38857156/how-to-query-many-to-many-relationship-sequelize
{
const rows = await Post.findAll({
include: [
{
model: User,
where: {id: user1.id},
through: {
where: {score: { [Op.gt]: 2 }},
},
},
],
})
assert.strictEqual(rows[0].body, 'post2');
// TODO how to get the score here as well?
//assert.strictEqual(rows[0].UserLikesPost.score, 3);
assert.strictEqual(rows.length, 1);
}
})().finally(() => { return sequelize.close() });
common.js
const path = require('path');
const { Sequelize } = require('sequelize');
function sequelize(filename, dialect, opts) {
if (dialect === undefined) {
dialect = 'l'
}
if (dialect === 'l') {
return new Sequelize(Object.assign({
dialect: 'sqlite',
storage: path.parse(filename).name + '.sqlite'
}, opts));
} else if (dialect === 'p') {
return new Sequelize('tmp', undefined, undefined, Object.assign({
dialect: 'postgres',
host: '/var/run/postgresql',
}, opts));
} else {
throw new Error('Unknown dialect')
}
}
exports.sequelize = sequelize
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"
}
}
tested on PostgreSQL 13.4, Ubuntu 21.04.

How to count a group by query in NodeJS Sequelize

In Rails I can perform a simple ORM query for the number of Likes a model has:
#records = Model
.select( 'model.*' )
.select( 'count(likes.*) as likes_count' )
.joins( 'LEFT JOIN likes ON model.id = likes.model_id' )
.group( 'model.id' )
This generates the query:
SELECT models.*, count(likes.*) as likes_count
FROM "models" JOIN likes ON models.id = likes.model_id
GROUP BY models.id
In Node Sequelize, any attempt at doing something similar fails:
return Model.findAll({
group: [ '"Model".id' ],
attributes: ['id', [Sequelize.fn('count', Sequelize.col('"Likes".id')), 'likes_count']],
include: [{ attributes: [], model: Like }],
});
This generates the query:
SELECT
Model.id,
count(Likes.id) AS likes_count,
Likes.id AS Likes.id # Bad!
FROM Models AS Model
LEFT OUTER JOIN Likes
AS Likes
ON Model.id = Likes.model_id
GROUP BY Model.id;
Which generates the error:
column "Likes.id" must appear in the GROUP BY clause or be used in an aggregate function
It's erroneously selecting likes.id, and I have no idea why, nor how to get rid of it.
This sequelize github issue looks totally like your case:
User.findAll({
attributes: ['User.*', 'Post.*', [sequelize.fn('COUNT', 'Post.id'), 'PostCount']],
include: [Post]
});
To resolve this problem we Need to upgrade to latest version of sequelize and include raw = true,
Here is How I had done after lot of iteration and off-course googling.
getUserProjectCount: function (req, res) {
Project.findAll(
{
attributes: ['User.username', [sequelize.fn('COUNT', sequelize.col('Project.id')), 'ProjectCount']],
include: [
{
model: User,
attributes: [],
include: []
}
],
group: ['User.username'],
raw:true
}
).then(function (projects) {
res.send(projects);
});
}
where my reference models are
//user
var User = sequelize.define("User", {
username: Sequelize.STRING,
password: Sequelize.STRING
});
//project
var Project = sequelize.define("Project", {
name: Sequelize.STRING,
UserId:{
type:Sequelize.INTEGER,
references: {
model: User,
key: "id"
}
}
});
Project.belongsTo(User);
User.hasMany(Project);
after migration ORM create 'Users' & 'Projects' table into my postgres server.
Here is SQL Query by ORM
SELECT
"User"."username", COUNT("Project"."id") AS "ProjectCount"
FROM
"Projects" AS "Project"
LEFT OUTER JOIN "Users" AS "User" ON "Project"."UserId" = "User"."id"
GROUP BY
"User"."username";
What worked for me counting column A and grouping by column B
const noListingsPerRetailer = Listing.findAll({
attributes: [
'columnA',
[sequelize.fn('COUNT', sequelize.col('columnB')), 'labelForCountColumn'],
],
group:["columnA"]
});

Resources