Sequelize query with count in inner join - node.js

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

Related

Cannot read property 'ClientSession' of undefined [duplicate]

I am using MongoDB Atlas cloud(https://cloud.mongodb.com/) and Mongoose library.
I tried to create multiple documents using transaction concept, but it is not working.
I am not getting any error. but, it seems rollback is not working properly.
app.js
//*** more code here
var app = express();
require('./models/db');
//*** more code here
models/db.js
var mongoose = require( 'mongoose' );
// Build the connection string
var dbURI = 'mongodb+srv://mydb:pass#cluster0-****.mongodb.net/mydb?retryWrites=true';
// Create the database connection
mongoose.connect(dbURI, {
useCreateIndex: true,
useNewUrlParser: true,
});
// Get Mongoose to use the global promise library
mongoose.Promise = global.Promise;
models/user.js
const mongoose = require("mongoose");
const UserSchema = new mongoose.Schema({
userName: {
type: String,
required: true
},
pass: {
type: String,
select: false
}
});
module.exports = mongoose.model("User", UserSchema, "user");
myroute.js
const db = require("mongoose");
const User = require("./models/user");
router.post("/addusers", async (req, res, next) => {
const SESSION = await db.startSession();
await SESSION.startTransaction();
try {
const newUser = new User({
//*** data for user ***
});
await newUser.save();
//*** for test purpose, trigger some error ***
throw new Error("some error");
await SESSION.commitTransaction();
//*** return data
} catch (error) {
await SESSION.abortTransaction();
} finally {
SESSION.endSession();
}
});
Above code works without error, but it still creates user in the DB. It suppose to rollback the created user and the collection should be empty.
I don't know what I have missed here. Can anyone please let me know whats wrong here?
app, models, schema and router are in different files.
You need to include the session within the options for all read/write operations which are active during a transaction. Only then are they actually applied to the transaction scope where you are able to roll them back.
As a bit more complete listing, and just using the more classic Order/OrderItems modelling which should be pretty familiar to most people with some relational transactions experience:
const { Schema } = mongoose = require('mongoose');
// URI including the name of the replicaSet connecting to
const uri = 'mongodb://localhost:27017/trandemo?replicaSet=fresh';
const opts = { useNewUrlParser: true };
// sensible defaults
mongoose.Promise = global.Promise;
mongoose.set('debug', true);
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
// schema defs
const orderSchema = new Schema({
name: String
});
const orderItemsSchema = new Schema({
order: { type: Schema.Types.ObjectId, ref: 'Order' },
itemName: String,
price: Number
});
const Order = mongoose.model('Order', orderSchema);
const OrderItems = mongoose.model('OrderItems', orderItemsSchema);
// log helper
const log = data => console.log(JSON.stringify(data, undefined, 2));
// main
(async function() {
try {
const conn = await mongoose.connect(uri, opts);
// clean models
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
)
let session = await conn.startSession();
session.startTransaction();
// Collections must exist in transactions
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.createCollection())
);
let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
let items = await OrderItems.insertMany(
[
{ order: order._id, itemName: 'Cheese', price: 1 },
{ order: order._id, itemName: 'Bread', price: 2 },
{ order: order._id, itemName: 'Milk', price: 3 }
],
{ session }
);
// update an item
let result1 = await OrderItems.updateOne(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ session }
);
log(result1);
// commit
await session.commitTransaction();
// start another
session.startTransaction();
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
/*
* $lookup join - expect Milk to be price: 4
*
*/
let joined = await Order.aggregate([
{ '$match': { _id: order._id } },
{ '$lookup': {
'from': OrderItems.collection.name,
'foreignField': 'order',
'localField': '_id',
'as': 'orderitems'
}}
]);
log(joined);
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})()
So I would generally recommend calling the variable session in lowercase, since this is the name of the key for the "options" object where it is required on all operations. Keeping this in the lowercase convention allows for using things like the ES6 Object assignment as well:
const conn = await mongoose.connect(uri, opts);
...
let session = await conn.startSession();
session.startTransaction();
Also the mongoose documentation on transactions is a little misleading, or at least it could be more descriptive. What it refers to as db in the examples is actually the Mongoose Connection instance, and not the underlying Db or even the mongoose global import as some may misinterpret this. Note in the listing and above excerpt this is obtained from mongoose.connect() and should be kept within your code as something you can access from a shared import.
Alternately you can even grab this in modular code via the mongoose.connection property, at any time after a connection has been established. This is usually safe inside things such as server route handlers and the like since there will be a database connection by the time that code is called.
The code also demonstrates the session usage in the different model methods:
let [order, other] = await Order.insertMany([
{ name: 'Bill' },
{ name: 'Ted' }
], { session });
let fred = new Order({ name: 'Fred' });
await fred.save({ session });
All the find() based methods and the update() or insert() and delete() based methods all have a final "options block" where this session key and value are expected. The save() method's only argument is this options block. This is what tells MongoDB to apply these actions to the current transaction on that referenced session.
In much the same way, before a transaction is committed any requests for a find() or similar which do not specify that session option do not see the state of the data whilst that transaction is in progress. The modified data state is only available to other operations once the transaction completes. Note this has effects on writes as covered in the documentation.
When an "abort" is issued:
// Update and abort
let result2 = await OrderItems.findOneAndUpdate(
{ order: order._id, itemName: 'Milk' },
{ $inc: { price: 1 } },
{ 'new': true, session }
);
log(result2);
await session.abortTransaction();
Any operations on the active transaction are removed from state and are not applied. As such they are not visible to resulting operations afterwards. In the example here the value in the document is incremented and will show a retrieved value of 5 on the current session. However after session.abortTransaction() the previous state of the document is reverted. Note that any global context which was not reading data on the same session, does not see that state change unless committed.
That should give the general overview. There is more complexity that can be added to handle varying levels of write failure and retries, but that is already extensively covered in documentation and many samples, or can be answered to a more specific question.
Output
For reference, the output of the included listing is shown here:
Mongoose: orders.deleteMany({}, {})
Mongoose: orderitems.deleteMany({}, {})
Mongoose: orders.insertMany([ { _id: 5bf775986c7c1a61d12137dd, name: 'Bill', __v: 0 }, { _id: 5bf775986c7c1a61d12137de, name: 'Ted', __v: 0 } ], { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orders.insertOne({ _id: ObjectId("5bf775986c7c1a61d12137df"), name: 'Fred', __v: 0 }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orderitems.insertMany([ { _id: 5bf775986c7c1a61d12137e0, order: 5bf775986c7c1a61d12137dd, itemName: 'Cheese', price: 1, __v: 0 }, { _id: 5bf775986c7c1a61d12137e1, order: 5bf775986c7c1a61d12137dd, itemName: 'Bread', price: 2, __v: 0 }, { _id: 5bf775986c7c1a61d12137e2, order: 5bf775986c7c1a61d12137dd, itemName: 'Milk', price: 3, __v: 0 } ], { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
Mongoose: orderitems.updateOne({ order: ObjectId("5bf775986c7c1a61d12137dd"), itemName: 'Milk' }, { '$inc': { price: 1 } }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2") })
{
"n": 1,
"nModified": 1,
"opTime": {
"ts": "6626894672394452998",
"t": 139
},
"electionId": "7fffffff000000000000008b",
"ok": 1,
"operationTime": "6626894672394452998",
"$clusterTime": {
"clusterTime": "6626894672394452998",
"signature": {
"hash": "AAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"keyId": 0
}
}
}
Mongoose: orderitems.findOneAndUpdate({ order: ObjectId("5bf775986c7c1a61d12137dd"), itemName: 'Milk' }, { '$inc': { price: 1 } }, { session: ClientSession("80f827fe077044c8b6c0547b34605cb2"), upsert: false, remove: false, projection: {}, returnOriginal: false })
{
"_id": "5bf775986c7c1a61d12137e2",
"order": "5bf775986c7c1a61d12137dd",
"itemName": "Milk",
"price": 5,
"__v": 0
}
Mongoose: orders.aggregate([ { '$match': { _id: 5bf775986c7c1a61d12137dd } }, { '$lookup': { from: 'orderitems', foreignField: 'order', localField: '_id', as: 'orderitems' } } ], {})
[
{
"_id": "5bf775986c7c1a61d12137dd",
"name": "Bill",
"__v": 0,
"orderitems": [
{
"_id": "5bf775986c7c1a61d12137e0",
"order": "5bf775986c7c1a61d12137dd",
"itemName": "Cheese",
"price": 1,
"__v": 0
},
{
"_id": "5bf775986c7c1a61d12137e1",
"order": "5bf775986c7c1a61d12137dd",
"itemName": "Bread",
"price": 2,
"__v": 0
},
{
"_id": "5bf775986c7c1a61d12137e2",
"order": "5bf775986c7c1a61d12137dd",
"itemName": "Milk",
"price": 4,
"__v": 0
}
]
}
]
I think this is the quickest way to start performing transaction with mongoose
const mongoose = require("mongoose");
// starting session on mongoose default connection
const session = await mongoose.startSession();
mongoose.connection.transaction(async function executor(session) {
try {
// creating 3 collections in isolation with atomicity
const price = new Price(priceSchema);
const variant = new Variant(variantSchema);
const item = new Item(itemSchema);
await price.save({ session });
await variant.save({ session });
// throw new Error("opps some error in transaction");
return await item.save({ session });
} catch (err) {
console.log(err);
}
});

Sequelize throws Error "Unable to find a valid association for model x" When Ordering By Associated Model

I have a problem with sequelize, when I want to ordering my query result by associated model, sequelize throw this error:
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Unable to find a valid association for model, 'productLanguage'
These are my files:
**Context.js **
const Sequelize = require('sequelize');
const sequelize = new Sequelize("postgres://postgres:123456#localhost:5432/sampleDB");
module.exports = {
Sequelize: Sequelize,
sequelize: sequelize
}
User.js
const context = require('../context');
module.exports = context.sequelize.define('user', {
name: context.Sequelize.STRING,
},{
freezeTableName: true
});
Product.js
const context = require('../context');
module.exports = context.sequelize.define('product', {
slug: context.Sequelize.STRING,
price: context.Sequelize.DECIMAL(10,2),
},{
freezeTableName: true
});
ProductLanguage.js
const context = require('../context');
module.exports = context.sequelize.define('productLanguage', {
name: context.Sequelize.STRING,
},{
freezeTableName: true,
timestamps: false
});
Language.js
const context = require('../context');
module.exports = context.sequelize.define('language', {
name: context.Sequelize.STRING,
slug: context.Sequelize.STRING,
},{
freezeTableName: true
});
db.js
var context = require('./context');
var User = require('./models/User'),
Product = require('./models/Product'),
ProductLanguage = require('./models/ProductLanguage'),
Language = require('./models/Language');
// ===================== ASSOCIATIONS =====================
// user 1:m Product
Product.belongsTo(User); // product owner
User.hasMany(Product);
// Product 1:m ProductLanguage m:1 Language
ProductLanguage.belongsTo(Product);
Product.hasMany(ProductLanguage);
ProductLanguage.belongsTo(Language);
Language.hasMany(ProductLanguage);
module.exports = {
Sequelize: context.Sequelize,
sequelize: context.sequelize,
models: {
Product: Product,
User: User,
ProductLanguage: ProductLanguage,
Language: Language
}
}
and finally this is my query
app.get('/', async (req, res, next)=>{
var result = await db.models.User.findAll({
include:[
{
model: db.models.Product,
attributes: ['price'],
include: [
{
model: db.models.ProductLanguage,
attributes: ['name'],
include: [
{
model: db.models.Language,
attributes: ['name'],
}
]
}
]
}
],
order:[
[db.models.ProductLanguage, 'name', 'desc']
],
attributes: ['name']
});
res.send(result);
});
The query work fine without "order" part, so I think the problem should be one on these :
Something is wrong on this part: [db.models.ProductLanguage, 'name', 'desc']
Something is wrong on association definitions
Note: I've searched on youtube and stackoverflow and sequelize documentation over 4 days but nothing found.
I use these dependencies:
"express": "^4.16.2",
"pg": "^6.4.2",
"pg-hstore": "^2.3.2",
"sequelize": "^4.32.2"
I've found the solution.
I must put all associated model into order, so the correct query is:
order:[
[db.models.Product, db.sequelize.models.ProductLanguage, 'name', 'desc']
],
The full query must be:
var result = await db.models.User.findAll({
include:[
{
model: db.models.Product,
attributes: ['price'],
include: [
{
model: db.models.ProductLanguage,
attributes: ['name'],
include: [
{
model: db.models.Language,
attributes: ['name'],
}
]
}
]
}
],
order:[
[db.models.Product, db.sequelize.models.ProductLanguage, 'name', 'desc']
],
attributes: ['name']
});
I hope this will be helpful for others.
Those who still won't get the result, try this syntax -
order:[[{ model: db.models.ProductLanguage, as: 'language_of_product' } , 'name', 'desc']]
In addition to Moradof's answer, it's important to note that if you specify an alias for your included model, then you must also specify the alias in the order statement.
Building on the previous example, we get:
var result = await db.models.User.findAll({
include:[
{
model: db.models.Product,
as: 'include1',
attributes: ['price'],
include: [
{
model: db.models.ProductLanguage,
as: 'include2',
attributes: ['name'],
include: [
{
model: db.models.Language,
attributes: ['name'],
}
]
}
]
}
],
order:[
[{ model: db.models.Product, as: 'include1' },
{ model: db.sequelize.models.ProductLanguage, as: 'include2' },
'name',
'desc']
],
attributes: ['name']
});
Note that because I named the Product as include1 in the include statement, I also had to name it as include1 in the order statement.
order:[
[{ model: db.models.Product, as: 'include1' },
{ model: db.sequelize.models.ProductLanguage, as: 'include2' },
'name',
'desc']
After using this sequlizer is creating this in query. ORDER BY ``.name DESC LIMIT 10;
So how can I pass the table alias before name.

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.

nodejs + sequelize :: include where required false

i have the following hierarchy of objects:
user
parent (hasOne)
children (hasMany)
once i have a user object, im attempting to load the associated parent object along with some of its children. the following works:
user.getParent({
include: [{
model: Child
}]
}).then(function(parent) {
var children = parent.children;
});
but if i want to selectively load some of the parent's children, like so:
user.getParent({
include: [{
model: Child,
where: { gender: 'FEMALE' }
}]
}).then(function(parent) {
var daughters = parent.daughters;
});
if the parent has one or more daughters, the query works and i get the parent data along with all daughters' data. however, if the parent has only sons, the parent object returned is null..
i want that if the parent has no daughters, the parent object should still be resolved with children = null or children = []..
also, if i wanted to simply load a count of children while loading the parent, how can i go about doing that?
thanks
it turned out to be an issue with my own code.. sequelize fails to load a model if a nested include has a where clause that returns no models.. to ensure it doesnt fail completely, you can set a required: false clause along with the where clause.. this makes sequelize return a blank array instead of failing the load completely..
more info at https://github.com/sequelize/sequelize/issues/4019
required: false + where is inconsistent/buggy in sequelize 6.5.1 SQLite vs PostgreSQL
Edit: it was working fine as of sequelize v6.13.0, so it got fixed somewhere along the way.
This is one of the many many many Sequelize association bugs/inconsistencies that make me want to try and find another ORM at some point. Reported at: https://github.com/sequelize/sequelize/issues/13809
Consider the following example where we want to:
list all tags
in addition, determine if each tag has been assigned to a selected animal
Our where condition on the animal then is a simple "check if the animal is a specific animal", and can only return 0 or 1 entries therefore. But the same would be valid with any other more complex condition.
main.js
#!/usr/bin/env node
const assert = require('assert')
const path = require('path')
const { DataTypes, Sequelize } = require('sequelize')
let sequelize
if (process.argv[2] === 'p') {
sequelize = new Sequelize('tmp', undefined, undefined, {
dialect: 'postgres',
host: '/var/run/postgresql',
})
} else {
sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'tmp.sqlite'
})
}
;(async () => {
const Animal = sequelize.define('Animal', {
species: { type: DataTypes.STRING },
})
const Tag = sequelize.define('Tag', {
name: { type: DataTypes.STRING },
})
Animal.belongsToMany(Tag, { through: 'AnimalTag' })
Tag.belongsToMany(Animal, { through: 'AnimalTag' })
await sequelize.sync({force: true})
const animal0 = await Animal.create({ species: 'bat' })
const animal1 = await Animal.create({ species: 'ant' })
const animal2 = await Animal.create({ species: 'dog' })
const tag0 = await Tag.create({ name: 'mammal' })
const tag1 = await Tag.create({ name: 'flying' })
const tag2 = await Tag.create({ name: 'aquatic' })
await animal0.addTag(tag0)
await animal0.addTag(tag1)
await animal2.addTag(tag1)
const animals = [animal0, animal1, animal2]
for (let animal of animals) {
let rows
rows = await Tag.findAll({
include: [{
model: Animal,
where: { id: animal.id },
// No effect.
//on: { id: animal.id },
through: {
// Same as putting the where outside of through.
//where: { AnimalId: animal.id },
},
required: false,
}],
order: [['name', 'ASC']],
})
console.error(animal.species);
console.error(rows.map(row => { return {
name: row.name,
animals: row.Animals.map(animal => animal.species),
} }))
}
})().finally(() => { return sequelize.close() })
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"
}
}
Run:
npm install
./main.js p
./main.js
The PotsgreSQL results are as expected:
bat
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [ 'bat' ] },
{ name: 'mammal', animals: [ 'bat' ] }
]
ant
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [] },
{ name: 'mammal', animals: [] }
]
dog
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [] },
{ name: 'mammal', animals: [ 'dog' ] }
]
But the SQLite results are just plain wrong:
bat
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [ 'bat' ] },
{ name: 'mammal', animals: [ 'bat', null ] }
]
ant
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [] },
{ name: 'mammal', animals: [] }
]
dog
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [] },
{ name: 'mammal', animals: [] }
]
I don't mind the null that much, I can work around that.
But dog not being a mammal, that I cannot work around.
Underlying queries
We can look at the underlying queries to guess what is going on.
PostgreSQL has queries of type:
SELECT
"Tag"."name",
"Animals"."species" AS "Animals.species",
FROM
"Tags" AS "Tag"
LEFT OUTER JOIN (
"AnimalTag" AS "Animals->AnimalTag"
INNER JOIN "Animals" AS "Animals" ON "Animals"."id" = "Animals->AnimalTag"."AnimalId"
) ON "Tag"."id" = "Animals->AnimalTag"."TagId"
AND "Animals"."id" = 1
ORDER BY
"Tag"."name" ASC;
SQLite has queries of type:
SELECT
`Tag`.`name`,
`Animals`.`species` AS `Animals.species`
FROM `Tags` AS `Tag`
LEFT OUTER JOIN `AnimalTag` AS `Animals->AnimalTag`
ON `Tag`.`id` = `Animals->AnimalTag`.`TagId`
LEFT OUTER JOIN `Animals` AS `Animals`
ON `Animals`.`id` = `Animals->AnimalTag`.`AnimalId`
AND `Animals`.`id`= 3
ORDER BY
`Tag`.`name` ASC;
So only the PostgreSQL query is the desired one because of the way it uses the subquery so that the:
ON "Tag"."id" = "Animals->AnimalTag"."TagId"
AND "Animals"."id" = 1
gets applied only to the outer LEFT OUTER JOIN.
The SQLite query produces undesired results, running it manually for each animal we get outputs:
bat
aquatic|
flying|bat
mammal|bat
mammal|
ant
aquatic|
flying|
mammal|
mammal|
dot
aquatic|
flying|
mammal|
mammal|dog
so what seems to happen is that the animal first result is null, then sequelize ignores it on the return, which would explain why we saw:
{ name: 'mammal', animals: [ 'bat', null ] }
for bat but:
{ name: 'mammal', animals: [] }
for dog.
The desired statement would have the AND on the first LEFT OUTER JOIN:
SELECT
`Tag`.`name`,
`Animals`.`species` AS `Animals.species`
FROM `Tags` AS `Tag`
LEFT OUTER JOIN `AnimalTag` AS `Animals->AnimalTag`
ON `Tag`.`id` = `Animals->AnimalTag`.`TagId`
AND `Animals->AnimalTag`.`AnimalId`= 1
LEFT OUTER JOIN `Animals` AS `Animals`
ON `Animals`.`id` = `Animals->AnimalTag`.`AnimalId`
ORDER BY
`Tag`.`name` ASC;
Workaround: super many to many
The best workaround I could come up so far is to use super many to many, this is often a good bet when the problem is that we need greater control about ON conditions:
#!/usr/bin/env node
const assert = require('assert')
const path = require('path')
const { DataTypes, Sequelize } = require('sequelize')
let sequelize
let logging = process.argv[3] === '1'
if (process.argv[2] === 'p') {
sequelize = new Sequelize('tmp', undefined, undefined, {
dialect: 'postgres',
host: '/var/run/postgresql',
logging,
})
} else {
sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'tmp.sqlite',
logging,
})
}
;(async () => {
const AnimalTag = sequelize.define('AnimalTag')
const Animal = sequelize.define('Animal', {
species: { type: DataTypes.STRING },
})
const Tag = sequelize.define('Tag', {
name: { type: DataTypes.STRING },
})
AnimalTag.belongsTo(Animal)
Animal.hasMany(AnimalTag)
AnimalTag.belongsTo(Tag)
Tag.hasMany(AnimalTag)
Animal.belongsToMany(Tag, { through: AnimalTag })
Tag.belongsToMany(Animal, { through: AnimalTag })
await sequelize.sync({force: true})
const animal0 = await Animal.create({ species: 'bat' })
const animal1 = await Animal.create({ species: 'ant' })
const animal2 = await Animal.create({ species: 'dog' })
const tag0 = await Tag.create({ name: 'flying' })
const tag1 = await Tag.create({ name: 'mammal' })
const tag2 = await Tag.create({ name: 'aquatic' })
await animal0.addTag(tag0)
await animal0.addTag(tag1)
await animal2.addTag(tag1)
const animals = [animal0, animal1, animal2]
for (let animal of animals) {
let rows
rows = await Tag.findAll({
include: [{
model: AnimalTag,
where: { AnimalId: animal.id },
required: false,
include: [{
model: Animal,
}]
}],
order: [['name', 'ASC']],
})
console.error(rows.map(row => { return {
name: row.name,
animals: row.AnimalTags.map(animalTag => animalTag.Animal.species)
} }))
console.error();
}
})().finally(() => { return sequelize.close() })
produces the correct output for both PostgreSQL and Sequelize. The produced query is exactly what we wanted:
SELECT
"Tag"."id",
"Tag"."name",
"Tag"."createdAt",
"Tag"."updatedAt",
"AnimalTags"."createdAt" AS "AnimalTags.createdAt",
"AnimalTags"."updatedAt" AS "AnimalTags.updatedAt",
"AnimalTags"."AnimalId" AS "AnimalTags.AnimalId",
"AnimalTags"."TagId" AS "AnimalTags.TagId",
"AnimalTags->Animal"."id" AS "AnimalTags.Animal.id",
"AnimalTags->Animal"."species" AS "AnimalTags.Animal.species",
"AnimalTags->Animal"."createdAt" AS "AnimalTags.Animal.createdAt",
"AnimalTags->Animal"."updatedAt" AS "AnimalTags.Animal.updatedAt"
FROM
"Tags" AS "Tag"
LEFT OUTER JOIN "AnimalTags" AS "AnimalTags" ON "Tag"."id" = "AnimalTags"."TagId"
AND "AnimalTags"."AnimalId" = 3
LEFT OUTER JOIN "Animals" AS "AnimalTags->Animal" ON "AnimalTags"."AnimalId" = "AnimalTags->Animal"."id"
ORDER BY
"Tag"."name" ASC;
Tested on Ubuntu 21.10, PostgreSQL 13.5.

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.

Resources