Sequelize find by association through manually-defined join table - node.js

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.

Related

Show specific columns with dataloader-sequelize in nested tables

Currently I have some models. I'm using graphql with dataloader-sequelize and it works fine as long as I show associated tables without third level.
My models:
"articulo.js"
'use strict';
module.exports = (sequelize, DataTypes) => {
const Articulo = sequelize.define(
'articulos',
{
art_codigo: {
type: DataTypes.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
},
art_nombre: DataTypes.STRING(255),
art_longitud: DataTypes.STRING(250),
art_latitud: DataTypes.STRING(250),
.....[more columns]
art_contenido: DataTypes.TEXT,
},
{
timestamps: false,
freezeTableName: true,
name: {
singular: 'Articulo',
plural: 'Articulos',
},
indexes: [
{
unique: true,
fields: ['art_codigo'],
},
],
}
);
Articulo.associate = (models) => {
Articulo.belongsTo(models.canalizados,
{
foreignKey: 'art_canalizado',
as:"Canalizado",
}
);
Articulo.belongsTo(
models.articulos_tipos,
{
foreignKey: 'art_tipo'
}
);
};
return Articulo;
};
articulo_tipo.js
'use strict';
module.exports = (sequelize, DataTypes) => {
const ArticuloTipo = sequelize.define('articulos_tipos', {
ari_codigo: {
type: DataTypes.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
},
ari_nombre: DataTypes.STRING(255),
}, {
timestamps: false,
freezeTableName: true,
name: {
singular: 'ArticuloTipo',
plural: 'ArticulosTipos',
},
indexes: [
{
unique: true,
fields: ['ari_codigo'],
},
],
});
ArticuloTipo.associate = (models) => {
ArticuloTipo.hasMany(models.articulos)
};
return ArticuloTipo;
};
canalizado.js
'use strict';
module.exports = (sequelize, DataTypes) => {
const Canalizado = sequelize.define('canalizados', {
cnl_codigo: {
type: DataTypes.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
},
cnl_fecha_alta: DataTypes.DATE,
...... [more columns]
cnl_revisado: DataTypes.BOOLEAN,
}, {
timestamps: false,
freezeTableName: true,
name: {
singular: 'Canalizado',
plural: 'Canalizados',
},
indexes: [
{
unique: true,
fields: ['cnl_codigo'],
},
],
}
);
Canalizado.associate = (models) => {
Canalizado.hasMany(models.articulos);
Canalizado.belongsTo(
models.canalizados_tipos,
{
foreignKey: 'cnl_tipo',
}
);
};
return Canalizado;
};
canalizado_tipo.js
'use strict';
module.exports = (sequelize, DataTypes) => {
const CanalizadoTipo = sequelize.define('canalizados_tipos', {
cai_codigo: {
type: DataTypes.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
},
cai_nombre: DataTypes.STRING(50)
}, {
timestamps: false,
freezeTableName: true,
tableName: "canalizados_tipos",
name: {
singular: 'CanalizadoTipo',
plural: 'CanalizadosTipo',
},
indexes: [
{
unique: true,
fields: ['cai_codigo'],
},
],
});
CanalizadoTipo.associate = (models) => {
CanalizadoTipo.hasMany(models.canalizados)
};
return CanalizadoTipo;
};
My resolvers:
articulo.js
const Sequelize = require('sequelize');
const {detectarCampos} = require('../_extra/comunes'); //Optimize which columns you want to use in graphql
const Op = Sequelize.Op;
const resolvers = {
Articulo:{
art_tipo: (parent, args, { models, options }, info) => {
return parent.getArticuloTipo(options); //It's an internal getter from sequelize, isn't it?
},
art_canalizado: (parent, args, { models, options }, info) => {
return parent.getCanalizado(options); //It's an internal getter from sequelize, isn't it?
},
},
Query: {
async getArticulo(root, { codigo }, { models }, info) {
return models.articulos.findByPk(
codigo,
{attributes: detectarCampos(info),}
);
},
async getArticulos(root, { nombre, tipo}, { models, options }, info) {
var whereStatement = {};
if(nombre){
whereStatement.art_nombre = {[Op.like]: '%' + nombre + '%'};
}
if (tipo){
whereStatement.art_tipo = tipo;
}
return models.articulos.findAll({
attributes: detectarCampos(info),
where: whereStatement,
//limit: 10,
options
});
},
async getAllArticulos(root, args, { models }, info) {
return models.articulos.findAll( {
attributes: detectarCampos(info),
limit: 10,
});
},
},
Mutation: {
},
}
module.exports = resolvers
canalizado.js
const {detectarCampos} = require('../_extra/comunes');
const resolvers = {
Canalizado:{
cnl_tipo: (parent, args, { models, options }, info) => {
return parent.getCanalizadoTipo(options)
},
},
Query: {
async getCanalizado(root, { codigo }, { models, context }, info) {
return await models.canalizados.findByPk(codigo,
{attributes: detectarCampos(info),});
},
async getCanalizados(root, { tipo }, { models, options }, info) {
var whereStatement = {};
if (tipo)
whereStatement.cnl_tipo = tipo;
return models.canalizados.findAll({
attributes: detectarCampos(info),
where: whereStatement,
limit: 2,
options
});
},
async getAllCanalizados(root, args, { models, options }) {
return models.canalizados.findAll({
attributes: detectarCampos(info),
limit: 100,
options
});
},
},
Mutation: {
},
}
module.exports = resolvers
It works fine if I search in graphql with this sentence:
query{
getArticulos(tipo:2){
art_codigo
art_nombre
art_tipo{
ari_nombre
}
art_latitud
art_longitud
}
}
Executing (default): SELECT [art_codigo], [art_nombre], [art_tipo], [art_latitud], [art_longitud] FROM [articulos] AS [articulos] WHERE [articulos].[art_tipo] = 2;
Executing (default): SELECT [ari_codigo], [ari_nombre] FROM [articulos_tipos] AS [articulos_tipos] WHERE [articulos_tipos].[ari_codigo] IN (2);
On the other hand, if I try to look for in a deeper level, I get automatic names from columns I don't need to use:
query{
getArticulos(tipo:2){
art_codigo
art_nombre
art_tipo{
ari_nombre
}
art_canalizado{
cnl_codigo
}
art_latitud
art_longitud
}
}
Executing (default): SELECT [art_codigo], [art_nombre], [art_tipo], [art_latitud], [art_longitud] FROM [articulos] AS [articulos] WHERE [articulos].[art_tipo] = 2;
Executing (default): SELECT [ari_codigo], [ari_nombre] FROM [articulos_tipos] AS [articulos_tipos] WHERE [articulos_tipos].[ari_codigo] IN (2);
Executing (default): SELECT [cnl_codigo], [cnl_fecha_alta], [........], [cnl_revisado], [cnl_tipo], [cnl_fuente], [cnl_autor], [CanalizadoTipoCaiCodigo] FROM [canalizados] AS [canalizados] WHERE [canalizados].[cnl_codigo] IN (51357, 51365, 51379, [........], 63910);
In this case, in Graphql returns this error:
"message": "Invalid column name 'CanalizadoTipoCaiCodigo'.",
How can I ommite that field?? Could I use something like "attributes" to specify which attributes I'd like to show?? I tried to use it in resolvers, models... but always with no success
This error is the same if I look for a deep level:
query{
getArticulos(relevancia:2){
art_codigo
art_nombre
art_tipo{
ari_nombre
}
art_canalizado{
cnl_codigo
cnl_tipo{
cai_nombre
}
}
art_latitud
art_longitud
}
}
Any idea about my problem? Everrything is wellcome!!
UPDATE
server.js
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
const typeDefs = require('./configuracion/schema/typeDefs')
const resolvers = require('./configuracion/schema/resolvers')
const models = require('./configuracion/models')
const { createContext, EXPECTED_OPTIONS_KEY } = require('dataloader-sequelize');
const dataloaderContext = createContext(models.sequelize);
//const server = new ApolloServer({ typeDefs, resolvers, context: { models } });
const server = new ApolloServer({
typeDefs,
resolvers,
context: async () => ({
models,
options: { [ EXPECTED_OPTIONS_KEY ]: dataloaderContext },
}),
});
const app = express();
server.applyMiddleware({ app });
models.sequelize.authenticate().then((err) => {
console.log('*** MSG [server.js]: Successful Connection');
})
.catch((err) => {
console.log('*** ERROR [server.js]: No ha sido posible conectarse a la base de datos', err);
})
//models.sequelize.sync();
app.listen({ port: 3000 }, () =>
console.log(`** API ready at http://localhost:3000${server.graphqlPath} `)
);
configuracion/models/index.js
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
//const env = process.env.NODE_ENV || 'development';
const config = require('../config_sqlserver')
const db = {};
const sequelize = new Sequelize(config.db_database, config.db_user, config.db_password,
{
host: config.db_host,
port: config.DB_PORT, // <----------------The port number you copied
dialect: "mssql",
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
}
}
);
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
//const model = sequelize['import'](path.join(__dirname, 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;
resolver > articulo_tipo.js
const Sequelize = require('sequelize');
const {detectarCampos} = require('../_extra/comunes');
const Op = Sequelize.Op;
const resolvers = {
Query: {
async getArticuloTipo(root, { codigo }, { models, context }, info) {
return await models.articulos_tipos.findByPk(codigo, { attributes: detectarCampos(info)},);
},
async getArticulosTipos(_, { nombre, tipo }, { models }, info) {r
var whereStatement = {};
if(nombre)
whereStatement.ari_nombre = {[Op.like]: '%' + nombre + '%'};
if(tipo)
whereStatement.ari_codigo = tipo;
return models.articulos_tipos.findAll({
attributes: detectarCampos(info),
where: whereStatement,
});
},
async getAllArticulosTipos(root, args, { models }) {
return models.articulos_tipos.findAll()
},
},
Mutation: {
},
}
module.exports = resolvers
I don't use sequelize ... but I probably can point you in the right direction:
attributes are used already...
maybe not exactly the way you need ...
check what is returned from detectarCampos(info) in resolvers
Probably you'll find that info is undefined ... sometimes info is missing... why!?
art_canalizado: (parent, args, { models, options }, info) => {
return parent.getCanalizado(options); //It's an internal getter from sequelize, isn't it?
},
getCanalizado is called with options while normally it should be called with more arguments:
async getCanalizado(root, { codigo }, { models, context }, info) {
Fix:
Pass missging arguments - it should work (if detectarCampos works, of course).

How to join two tables? - Cannot read property 'getTableName' of undefined

Users Model File
'use strict';
module.exports = (sequelize, DataTypes) => {
const user = sequelize.define('user', {
"access_role_id":DataTypes.INTEGER,
"user_type":{
type: DataTypes.ENUM,
values: ['A', 'V', 'U']
},
"login_access":{
type: DataTypes.ENUM,
values: ['APP', 'WEB', 'BOTH']
},
"username": DataTypes.STRING,
"email": DataTypes.STRING,
"updated_at":DataTypes.DATE,
"created_at":DataTypes.DATE,
}, {
"timestamps": false,
"createdAt": false,
'updatedAt': false,
"underscored": true,
"freezeTableName": false,
});
user.associate = function(models) {
// associations can be defined here
models.user.belongsTo(models.product, {targetKey: 'id',foreignKey: 'created_by'});
};
return user;
};
User Controller File
const dbConfig = require( '../../../db/models/index');
let users =require('../../../db/models/user')(dbConfig.sequelizeDB,dbConfig.Sequelize);
module.exports = {
// GET /customer/:id
getCustomer: function(req, res, next) {
users.findAll({
include: [{
model: users.product,
}]
}).then(function (stores) {
if (stores.length === 0) {
res.json('There are no stores in the database');
}
res.json(stores);
});
},
};
I am unable to get the product table data. It always shows an error:
Unhandled rejection TypeError: Cannot read property 'getTableName' of undefined.
Why the product table is not fetched in users model? How can I access this product table data by the users model?
Update your User Controller File as
const dbConfig = require( '../../../db/models/index');
let users =require('../../../db/models/user')(dbConfig.sequelizeDB,dbConfig.Sequelize);
let product=require('../../../db/models/product')(dbConfig.sequelizeDB,dbConfig.Sequelize);
module.exports = {
// GET /customer/:id
getCustomer: function(req, res, next) {
users.findAll({
include: [{
model: product,
}]
}).then(function (stores) {
if (stores.length === 0) {
res.json('There are no stores in the database');
}
res.json(stores);
});
},
};

sequelize.js association accessor

I made API server with Node.js
Also I use sequelize.js(version 4) for communicate with MySQL.
My table structure is here.
[Article]
no(PK)
subject
content
created_at
updated_at
[Comment]
no(PK)
content
created_at
updated_at
article_no(FK to Article)
[index.controller.js]
import { Article, Comment } from '../model/model';
export const index = (req, res) => {
res.send('controller index');
};
export const getArticle = (req, res) => {
try {
Article.all()
.then(article => {
res.status(200).json({status: true, result: article});
});
} catch(e) {
res.status(500).json({status: false, result: "get article fail"});
}
}
export const addArticle = (req, res) => {
const { subject, content } = req.body;
try {
Article.create({
subject: subject,
content: content
})
res.status(200).json({status: true, result: "article write success"});
} catch(e) {
res.status(500).json({status: false, result: "article fail"});
}
}
export const getComment = (req, res) => {
try {
Comment.all()
.then(comment => {
res.status(200).json({status: true, result: comment})
});
} catch(e) {
res.status(500).json({status: false, result: "get comment fail"});
}
}
export const addComment = (req, res) => {
const { content, article_no } = req.body;
try {
Comment.create({
content: content,
article_no: article_no
})
.then(() => res.status(200).json({status: true, result: "comment write success"}))
} catch(e) {
console.log(e);
res.status(500).json({status: false, result: "comment fail"});
}
}
[index.js]
import express from 'express';
import { index, getArticle, getComment,addArticle, addComment } from './index.controller';
const router = express.Router();
router.get('/', index);
router.get('/article', getArticle);
router.post('/article', addArticle);
router.get('/comment', getComment);
router.post('/comment', addComment);
export default router;
[model.js]
import Sequelize from 'sequelize';
const sequelize = new Sequelize('db', 'id', 'pw', {
host: '127.0.0.1',
dialect: 'mysql'
})
export const Article = sequelize.define('article', {
no: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
subject: {
type: Sequelize.STRING,
allowNull: false
},
content: {
type: Sequelize.STRING,
allowNull: false
}
}, {
freezeTableName: true,
underscored: true
})
export const Comment = sequelize.define('comment', {
no: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
content: {
type: Sequelize.STRING,
allowNull: false
}
}, {
freezeTableName: true,
underscored: true
})
Article.hasMany(Comment, {as: 'Comments'}); // association
Comment.belongsTo(Article); // association
sequelize.sync({
force: false
});
Because of association(hasMany, belongsTo), article_no column will be added to Comment table.
Refer to this document, http://docs.sequelizejs.com/manual/tutorial/associations.html#one-to-many-associations-hasmany-
It says that Instances of Project will get the accessors getWorkers and setWorkers.
In my case, it will be getComments and setComments.
But I don't know exactly how can I get all the comments related articles with using accessor.
Current output is here. (If I connect to GET /article)
{
"status":true,
"result":[
{
"no":1,
"content":"comment test",
"created_at":"2018-07-18T05:00:45.000Z",
"updated_at":"2018-07-18T05:00:45.000Z",
"article_no":1
}
]
}
Desired output is here
{
"status":true,
"result":[
{
"no":1,
"content":"comment test",
"created_at":"2018-07-18T05:00:45.000Z",
"updated_at":"2018-07-18T05:00:45.000Z",
"article_no":1,
"comments": [
// related comments here!
]
}
]
}
Thanks.
When you want to join another model you should use include in your query
User.findAll({
include: [
{ model: Profile, required: true // inner join }
],
limit: 3
});
Check out the Sequelize model usage docs.
To access the comments with accessors you will need do something like this:
const articles = await Article.all();
articles.forEach(article => {
const comments = await article.getComments();
})
The idea behind is that each article sequelize object will have the accessor getComments but internally what it does when you execute getComments it makes a new request to the database with the prepopulated articleId in the comments where query. This is called lazy loading because you can load the data when you need it. But that is not your case.
For the desired output I suggest to use the include method cause it will make a single request to the database.

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 query with count in inner join

I am trying to convert this query to sequelize query object what is the right wayto do it?
SELECT families.id, count('answers.familyId') FROM families LEFT JOIN
answers on families.id = answers.familyId WHERE answers.isActive=1 AND
answers.answer=1 GROUP BY families.id HAVING COUNT('answers.familyId')>=6
Let's assume that Family is your families sequelize model and Answer is your answers sequelize model, and sequelize is your Sequelize instance
Family.findAll({
attributes: ['*', sequelize.fn('COUNT', sequelize.col('Answers.familyId'))],
include: [
{
model: Answer,
attributes: [],
where: {
isActive: 1,
answer: 1
}
}
],
group: '"Family.id"',
having: sequelize.where(sequelize.fn('COUNT', sequelize.col('Answers.familyId')), '>=', 6)
}).then((families) => {
// result
});
Useful documentation links:
sequelize.fn()
sequelize.where()
sequelize.col()
You need to use get() on the attribute: aliased count column
There are two important gotchas when reading the aggregates out:
the count only shows up on results if you alias it with attributes as shown by Piotr at https://stackoverflow.com/a/42472696/895245 and as shown at How do I select a column using an alias attributes aliasing has the unexpected effect of requiring you to use .get().
as mentioned at: How does group by works in sequelize? the count comes out as a string in PostgreSQL due to bigint shenanigans, and you need parseInt it
Here's a minimal runnable example where we have posts and users who can like posts, and we want to count how:
how many likes each user has
ignoring likes to post2
considering only users that have 0 or 1 likes in total
The following small improvements are made over Piotr's code:
you likely don't want attributes: ['*' because that selects all columns, and therefore generally includes columns that are neither aggregates nor grouped by, leading to indeterminate behavior in some DBMSs and errors in others. You should just specify the GROUP by column instead, in our case the column is name.
using the slightly cleaner Op.lte rather than the literal '<='
Due to required: false, this first version does a LEFT OUTER JOIN + COUNT(column), see also: https://dba.stackexchange.com/questions/174694/how-to-get-a-group-where-the-count-is-zero
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.
If we wanted the INNER JOIN version excluding 0 counts, we could just remove the required: false, which makes it be the default true. We can also use do a slightly simpler COUNT(*) since there will be no NULLs now:
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)
PostgreSQL support has been broken for several years due to column X must appear in the GROUP BY clause or be used in an aggregate function
The above code should work for PostgreSQL too, but as mentioned at:
https://github.com/sequelize/sequelize/issues/3256
https://github.com/sequelize/sequelize/issues/5481#issuecomment-964387232
there's a bug and it doesn't. The fact that such glaring bugs have persisted for several years make me doubt if I should really be using this ORM.
The workaround is to use both:
raw: true,
includeIgnoreAttributes: false,
Full working example with the workaround:
#!/usr/bin/env node
const assert = require('assert');
const { DataTypes, Op, Sequelize } = require('sequelize');
const sequelize = new Sequelize('tmp', undefined, undefined, Object.assign({
dialect: 'postgres',
host: '/var/run/postgresql',
}));
;(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', '*'), 'count'],
],
raw: true,
includeIgnoreAttributes: false,
include: [
{
model: Post,
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].count, 10), 1)
assert.strictEqual(rows.length, 1)
})().finally(() => { return sequelize.close() });
tested on PostgreSQL 13.4, Ubuntu 21.10.
Related
Counting associated entries with Sequelize

Resources