How to aggregate from nested tables in Sequelize? - mysql

I have the following demo code written in Sequelize. I'd like to pull a list of Endpoint entities get a list of EndpointCall entities grouped by 1 minute time buckets. I can't seem to be able to solve this is Sequelize with a findAll with a nested table:
const Sequelize = require('sequelize');
const sequelize = new Sequelize(process.env.MYSQL_DATABASE, process.env.MYSQL_USER, process.env.MYSQL_PASSWORD, {
host: process.env.MYSQL_HOST,
dialect: 'mysql',
logging: false,
benchmark: false,
});
const model = {
Endpoint: sequelize.define('endpoint', {
id: { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true },
name: { type: Sequelize.STRING('256'), allowNull: false },
}),
EndpointCall: sequelize.define('endpoint_call', {
endpoint_id: { type: Sequelize.INTEGER, allowNull: false },
date: { type: Sequelize.DATE, allowNull: false },
response_time: { type: Sequelize.DOUBLE, allowNull: true },
}),
init: function () {
this.Endpoint.hasMany(this.EndpointCall, { foreignKey: 'endpoint_id' });
return sequelize.sync();
},
};
model.init()
.then(async () => {
var result = await model.Endpoint.findAll({
include: [{
model: model.EndpointCall,
required: false,
attributes: [
[Sequelize.literal(`FROM_UNIXTIME(ROUND(UNIX_TIMESTAMP(date)/60)*60)`), 'time_bucket'],
[Sequelize.literal(`AVG(response_time)`), 'avg_response_time']
],
group: 'time_bucket'
}],
});
console.log(result);
});
This is the closest I got to but this still throws the following error:
In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'database.endpoint.id'; this is incompatible with sql_mode=only_full_group_by
I don't necessarily want to configure the MySQL server a non-standard way, therefore I was trying to play with the group clause but with no luck:
group: [ 'id', 'name', 'time_bucket' ]
Moving the group clause to the top level table also didn't help.
UPDATE:
Tried group: [ 'endpoint_call.time_bucket' ] on the top level
Result:
SequelizeDatabaseError: Unknown column 'endpoint_call.time_bucket' in 'group statement'
Tried group: [ 'endpoint_calls.time_bucket' ] on the top level (with the -s at the end)
Result:
SequelizeDatabaseError: Unknown column 'endpoint_calls.time_bucket' in 'group statement'
WORKAROUND:
Thanks to Emma in the comments I found a workaround. I still thing that Sequelize should solve this internally, I'm publishing it here in case someone else gets stuck.
The point is removing sql_mode=only_full_group_by in a local session by executing this before the aggregated query:
SET sql_mode=(SELECT REPLACE(##sql_mode,'ONLY_FULL_GROUP_BY',''));
The full code is here:
model.init()
.then(async () => {
await sequelize.query(`SET sql_mode=(SELECT REPLACE(##sql_mode,'ONLY_FULL_GROUP_BY',''));`);
var result = await model.Endpoint.findAll({
include: [{
model: model.EndpointCall,
required: false,
separate: true,
attributes: [
[Sequelize.literal(`FROM_UNIXTIME(ROUND(UNIX_TIMESTAMP(date)/60)*60)`), 'time_bucket'],
[Sequelize.literal(`AVG(response_time)`), 'avg_response_time']
],
group: [ 'endpoint_id', 'time_bucket' ],
}],
});
console.log(result);
});

Related

Nodejs Sequelize

I have these 2 models:
Orders Models
Solutions model
Orders Model
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Orders extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
Orders.hasMany(models.Payments, {
foreignKey: {
name: 'order',
allowNull: false,
},
constraints: false,
onDelete: 'cascade',
});
Orders.hasOne(models.Solutions, {
foreignKey: {
name: 'order',
allowNull: false,
},
constraints: false,
onDelete: 'cascade',
as: "solution"
});
}
}
Orders.init(
{
order_no: {
defaultValue: DataTypes.UUIDV4,
type: DataTypes.UUID,
primaryKey: true,
allowNull: false,
unique: true,
},
order_date: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
},
{
sequelize,
modelName: 'Orders',
tableName: 'Orders',
}
);
return Orders;
};
#2. Solutions table
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Solutions extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
Solutions.belongsTo(models.Orders, {
foreignKey: 'order',
onDelete: 'cascade',
constraints: false,
as: "solution"
});
}
}
Solutions.init(
{
solutionId: {
defaultValue: DataTypes.UUIDV4,
type: DataTypes.UUID,
primaryKey: true,
allowNull: false,
unique: true,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
additional_instruction: {
type: DataTypes.TEXT,
allowNull: true,
},
date_submited: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
},
{
sequelize,
modelName: 'Solutions',
}
);
return Solutions;
};
I am trying to get all orders where it's solution has not been submitted to the solutions table, i.e order field(Foreign key in solution table) is null.
I have tried this
Orders.findAndCountAll({
include: [
{
model: Users,
attributes: ['username', 'email', 'uid'],
},
{
model: Solutions,
as: "solution",
where: {
solutionId: {
[Op.notIn]: Solutions.findAll({
attributes: ['solutionId']
})
}
}
}
],
offset: page,
limit,
})
I was expecting to get a list of all orders where the solutions in the solution table has not been added. Am a bit new to sequelize.
You can try to filter after left join, Sequelize can apply where clause directly on the join or after join.
Orders.findAndCountAll({
where: {
'$orders.solution$': null,
},
include: [
{
model: Solutions,
as: "solution",
required: false
},
],
})
In SQL it's like :
SELECT COUNT(*)
FROM orders o
LEFT JOIN solutions s ON o.id = s.order AND s.order IS NULL
VS
SELECT COUNT(*)
FROM orders o
LEFT JOIN solutions s ON o.id = s.order
WHERE s IS NULL
You can perform a left join with a filter which excludes records from Solutions table if the order does not exit.
Orders.findAndCountAll({
include: [
{
model: Users,
attributes: ['username', 'email', 'uid'],
},
{
model: Solutions,
as: "solution",
required: false,
},
],
where: {
'$solution.order$': null
},
offset: page,
limit,
})
For those coming later to this question, I have come to the conclusion that a LEFT OUTER JOIN between the two tables performs the exact same thing I was looking for. I want to give credit back to #Shengda Liu and #8bitIcon for the solution given.
In sequelize the solution would involve just adding the required field in the include statement on the target model to enforce the rule(i.e) find all rows that have an associations in the target associated table. For my case, the solution is as follows.
Orders.findAndCountAll({
include: [
{
model: Users,
attributes: ['username', 'email', 'uid'],
},
{
model: Solutions,
as: "solution",
required: true, // this is the only thing I have added
/// and removed the where clause in the include field.
},
],
offset: page,
limit,
})

Convert order by additional operator to sequelize

I want to convert query MySQL into sequelize, but i don't know
SELECT * FROM `RewardEntries`
ORDER BY `RewardEntries`.`happinessPoint` + `RewardEntries`.`moneyPoint` + `RewardEntries`.`healthyPoint` DESC
Please help me
thanks
There might be a more elegant way to do this. I've tested this with PostgreSQL, although the same would probably work for MySQL, too.
let rewards = await RewardEntry.findAll({
order: [
[ sequelize.literal('happinesspoint + moneypoint + healthypoint'), 'DESC' ]
]
})
Here's a complete sample. The name of the table in the database and all of its columns are all lowercase. That's why the query has just lowercase in it.
let {
Sequelize,
DataTypes,
} = require('sequelize')
async function run () {
let sequelize = new Sequelize('mydb', 'username', 'password', {
host: 'localhost',
port: 5432,
dialect: 'postgres',
logging: console.log
})
let RewardEntry = sequelize.define('RewardEntry', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true
},
HappinessPoint: {
type: DataTypes.INTEGER,
field: 'happinesspoint'
},
MoneyPoint: {
type: DataTypes.INTEGER,
field: 'moneypoint'
},
HealthyPoint: {
type: DataTypes.INTEGER,
field: 'healthypoint'
}
},
{
tableName: 'rewardentries',
timestamps: false
})
let rewards = await RewardEntry.findAll({
order: [
[ sequelize.literal('happinesspoint + moneypoint + healthypoint'), 'DESC' ]
]
})
console.log(JSON.stringify(rewards, null, 2))
await sequelize.close()
}
run()

Problem using both include and attributes in findAll() query in Sequelize

I searched on the internet a common issue but I did not find anything so I post the question here.
I am using Sequelize for Node and I am trying to query my PostgreSQL database to find all the objects in a specific table. However, I would like to be able to replace de foreign keys by data from their linked table (In other words, join tables and remove the ids). I managed to do this by using both the "include" option to join the table and the "attributes" option to unselect the Id and only keep the joined value, but since I have a hasMany relationship, I get an error when executing the query.
Here is the options I use :
options = {
include: [
{
model: models.ProviderType,
attributes: ['id', 'name'],
as: 'providerType',
},
{
model: models.SavingProduct,
attributes: ['id', 'name'],
as: 'savingProducts',
},
],
attributes: [
'id',
'name',
'siteUrl',
'logoPath',
'createdAt',
'updatedAt',
'deletedAt',
],
}
And it executed the following query :
SELECT "Provider".*, "providerType"."id" AS "providerType.id", "providerType"."name" AS "providerType.name", "savingProducts"."id" AS "savingProducts.id", "savingProducts"."name" AS "savingProducts.name"
FROM (
SELECT "Provider"."id", "Provider"."name", "Provider"."siteUrl", "Provider"."logoPath", "Provider"."createdAt", "Provider"."updatedAt", "Provider"."deletedAt"
FROM "Provider" AS "Provider"
WHERE "Provider"."deletedAt" IS NULL
LIMIT 20 OFFSET 0
) AS "Provider"
LEFT OUTER JOIN "ProviderType" AS "providerType" ON "Provider"."providerTypeId" = "providerType"."id"
LEFT OUTER JOIN "SavingProduct" AS "savingProducts" ON "Provider"."id" = "savingProducts"."providerId" AND ("savingProducts"."deletedAt" IS NULL);
The error thrown is : "column Provider.providerTypeId does not exist". It is logical because, when selecting the embedded SELECT, I do not select Provider.providerTypeId as I don't want this column to be returned, and because of that, when trying to JOIN the tables, SQL do not find this column and return an error.
In other words, Sequelize first select the wanted values and then join while I would like it to do the opposite, ie join the tables and then select only the asked columns.
So I understand the error but I don't find any way to replace the foreign keys by data from the linked tables. It is possible I did not take the right direction to do that so if you see something easier and cleaner, I would really appreciate!
Followed are the models definition :
For Provider
module.exports = (sequelize, DataTypes) => {
const Provider = sequelize.define('Provider', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
siteUrl: {
type: DataTypes.STRING,
allowNull: true,
},
logoPath: {
type: DataTypes.STRING,
allowNull: true,
},
}, {
freezeTableName: true,
paranoid: true,
});
Provider.associate = (models) => {
Provider.belongsTo(models.ProviderType, { as: 'providerType' });
Provider.hasMany(models.SavingProduct, { as: 'savingProducts', foreignKey: 'providerId' });
};
return Provider;
};
For SavingProduct
module.exports = (sequelize, DataTypes) => {
const SavingProduct = sequelize.define('SavingProduct', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
}, {
freezeTableName: true,
paranoid: true,
});
SavingProduct.associate = (models) => {
SavingProduct.belongsTo(models.Provider, { as: 'provider', foreignKey: 'providerId' });
};
return SavingProduct;
};
Please do not hesitate to tell me if I am not clear, I would be happy to provide you with more informations.
Thank you very much!

Dynamic define database in Sequelize for multi-tenant support return wrong query syntax

I'm Working on Multi-tenant Application (SAAS) with Shared Database Isolated Schema principle.
I've tried solution from https://github.com/renatoargh/data-isolation-example
from this article https://renatoargh.wordpress.com/2018/01/10/logical-data-isolation-for-multi-tenant-architecture-using-node-express-and-sequelize/
This is My Sequelize Model using schema Option
module.exports = (sequelize, DataTypes) => {
const Task = sequelize.define('Task', {
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
field: 'Id'
},
description: {
type: DataTypes.STRING(100),
allowNull: false,
field: 'Description'
},
done: {
type: DataTypes.BOOLEAN,
allowNull: false,
default: false,
field: 'Done'
},
taskTypeId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'TaskTypeId'
},
userId: {
type: DataTypes.INTEGER,
allowNull: true,
field: 'UserId'
}
}, {
freezeTableName: true,
tableName: 'Tasks',
createdAt: false,
updatedAt: false
})
Task.changeSchema = schema => Task.schema(schema)
Task.associate = models => {
Task.belongsTo(models.TaskType, {
as: 'taskType',
foreignKey: 'taskTypeId'
})
}
return Task
}
And Stop at this problem
SELECT
`Task`.`Id` AS `id`,
`Task`.`Description` AS `description`,
`Task`.`Done` AS `done`,
`Task`.`TaskTypeId` AS `taskTypeId`,
`Task`.`UserId` AS `userId`,
`taskType`.`Id` AS `taskType.id`,
`taskType`.`Description` AS `taskType.description`
FROM `tenant_1.Tasks` AS `Task` LEFT OUTER JOIN `shared.TaskTypes` AS `taskType`
ON `Task`.`TaskTypeId` = `taskType`.`Id`
WHERE `Task`.`UserId` = 1;
as you see, FROM `tenant_1.Tasks` in mysql is a wrong syntax. it must be FROM `tenant_1`.`Tasks`
how to change `tenant_1.Tasks` to `tenant_1`.`Tasks`
Are you using MySQL? If so, that is the expected behavior.
From the documentation of Model.schema:
Apply a schema to this model. For postgres, this will actually place the schema in front of the table name - "schema"."tableName", while the schema will be prepended to the table name for mysql and sqlite - 'schema.tablename'.

Counting associated entries with Sequelize

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