How to limit includes on a belongsToMany association in Sequelize JS? - mysql

I currently have the following models in Sequelize:
Product
and
ProductList.belongsToMany(models.Product, { as: 'products', through:
sequelize.models.ProductListProduct, foreignKey: 'productListId'});
and the pivot, ProductListProduct
I am currently trying to get a series of productList to show on my homepage,
and would need to limit the returned products to a certain value (6 in this case):
let productLists = await ProductList.findAll({
where: {
slug: ['recommended', 'new_and_promos']
},
include: [{
model: Product,
as: 'products',
include: ['attributes', 'images'],
where: {
active: true
}
}],
order: [
[
'products', ProductListProduct, 'position', 'ASC'
]
]
})
This is the current fetch, however if I add a limit to the include, it tells me that only hasMany can have {separate: true} ;
To recap, what I'm trying to achieve is to return n ProductList, each with just m Product attached.

Managed to get it working like this, doesn't look ideal but it does the job:
let productLists = await ProductList.findAll({
where: {
slug: ['recommended', 'new_and_promos']
},
include: [{
model: Product,
as: 'products',
include: ['attributes', 'images'],
where: Sequelize.literal('`products->ProductListProduct`.`position` < 8')
}],
order: [
[
'products', ProductListProduct, 'position', 'ASC'
]
]
})

Related

sequelize aggregate group by not working as I want

I want to fetch Votes Info with relations in a group according to audioId, but my code not working as aspected. 'user. id' break the group, and showing one by one vote.
what is the problem with my code? and how to group with only audioId and also fetch related Data
try {
const votes = await globalVotes.findAll({
group: ['audioId', 'mediaType', 'audio.id', 'user.id'],
attributes: [
'audioId',
'mediaType',
[Sequelize.fn('SUM', Sequelize.col('totalVote')), 'total_vote'],
[Sequelize.fn('MAX', Sequelize.col('voterEmail')), 'latest_email'],
[Sequelize.fn('COUNT', Sequelize.col('voteCount')), 'total_vote_count'],
[Sequelize.fn('MAX', Sequelize.col('vottingNum')), 'latest_number'],
[Sequelize.fn('MAX', Sequelize.col('globalVotes.user_Id')), 'latest_user'],
[
Sequelize.fn('MAX', Sequelize.col('globalVotes.updatedAt')),
'latest_time',
],
],
include: [
{
model: Audio,
attributes: ['aTitle', 'aImage', 'audioFolderId'],
include: [
{
model: AudioFolder,
attributes: ['folderName'],
},
],
group: ['audioFolderId.id'],
},
{
model: User,
attributes: ['fullName'],
},
],
order: [[Sequelize.fn('SUM', Sequelize.col('totalVote')), 'DESC']],
raw: true,
nest: true,
});
console.log(votes);
res.status(200).render('admin/chart/audio/index', {
title: 'Manage Audio Votes',
votes,
});
hope i get some info,

Sequelize - Include all children if any one matches

I have two entities, Post and Tag, I am trying to query for all posts that have any one tag passed to the where clause. In addition, I want to include ALL the tags for the final set of Posts.
The association is defined as so
Post.belongsToMany(
models.tag,
{
through: 'post_tag'
}
);
My query is like so
models.post.findAll({
limit: 20,
offset: 0,
attributes: [
'id',
'name'
],
include: [{
model: models.tag,
attributes: ['name'],
where: {
name: {
[Op.in]: ['tagNameHere']
}
}
}],
where: [{
active: {
[Op.not]: 'False'
}
}],
order: [ ['name', 'ASC'] ]
})
It does work, but the included tags array is ONLY that one specified within the Op.in. I want ALL the tags to be included
Any better way of going about it?
One approach is to make two passes: 1) find posts that have particular tag, 2) find all tags for those posts. You need a third association to make this happen:
models.post.belongsToMany(models.tag, {through: models.postTag, foreignKey: 'post_id'} );
models.tag.belongsToMany (models.post,{through: models.postTag, foreignKey: 'tag_id' });
models.post.hasOne(Post, {
foreignKey: {name: 'id'},
as: 'selfJoin'
});
Now, identify posts that have particular tag (or tags)
models.post.addScope('hasParticularTag',
{
attributes: ['id'],
include: [
{
model: models.tag,
through: models.postTag,
attributes: [],
where: {name: 'TAG-YOU-WANT'} // your parameter here...
}]
});
Finally, list selected posts and all their tasks...
models.post.findAll({
attributes: ['id','name'],
include: [
{ // ALL tags
model: models.tag,
through: models.postTag,
attributes: ['name']
},
{ // SELECTED posts
model: models.post.scope('hasParticularTag'),
required: true,
as: 'selfJoin', // prevents error "post isn't related to post"
attributes: []
}]
})
HTH....

How to fix Sequelize nested includes not working with limit/order/attributes?

I've got some models that have associations with each other, and I need to grab them all in a certain request. I need to use limit, order, and attributes on basically all parts of it, but this is causing the nested includes to freak out and I'm not entirely sure what's wrong with it.
It doesn't really print any errors or anything, the models either just don't get included in the response (i.e they are empty.), or they get included but stuff like order/limit is ignored.
I've already tried using subQuery, separate, etc... None of those worked.
The query in question;
const categories = await models.Category.findAll({
attributes: ['id', 'title', 'description'],
order: [['title', 'ASC']],
include: [
{
model: models.Product,
attributes: ['id', 'title'],
through: { attributes: [] },
include: [
{
model: models.Price,
attributes: ['id', 'amount', 'createdAt'],
order: [['createdAt', 'DESC']],
limit: 1,
},
],
},
],
});
The associations;
models.Category.belongsToMany(models.Product);
models.Product.belongsToMany(models.Category);
models.Product.hasMany(models.Price);
models.Price.belongsTo(models.Product);
I ideally want the query provided above to return;
Category with the order of Ascending based on title.
Product inside of Category with the attributes id and title.
Price inside of Product with the attributes id, amount, and createdAt, the order of Descending based on createdAt, and with a limit of 1.
In order to get the query to sort by Product.Price.createdAt, add [models.Product, models.Price, 'createdAt', 'DESC'] to order. As far as limiting: in order to limit the included model, you need to run it as a separate query, so add separate: true to the include.
Code:
const categories = await models.Category.findAll({
attributes: ['id', 'title', 'description'],
order: [['title', 'ASC'], [models.Product, models.Price, 'createdAt', 'DESC']],
include: [
{
model: models.Product,
attributes: ['id', 'title'],
through: { attributes: [] },
include: [
{
model: models.Price,
attributes: ['id', 'amount', 'createdAt'],
separate: true,
limit: 1,
},
],
},
],
});

Sequelize include even if it's null

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

Counting associated entries with Sequelize

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