Sequelize.js query to get total count through relationship - javascript

I'm using sequelize to get a total count through a relationship. I need it by a customerId that is in a parent table joined through a pivot table. The plain query looks something like this:
SELECT count(p.*) FROM parcels as p
LEFT JOIN orders_parcels as op ON op."parcelId" = p.id
LEFT JOIN orders as o ON op."orderId" = o.id
WHERE o."customerId"=1
This works fine. But not sure how to get the sequelize query.
Parcel.findAndCountAll();
EDIT: OrderParcel
var OrderParcel = service.sequelize.define('OrderParcel', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
}
}, {
tableName: 'orders_parcels',
freezeTableName: true,
paranoid: true
});
module.exports = OrderParcel;
var Order = require('./Order');
OrderParcel.belongsTo(Order, {
as: 'Order',
foreignKey: 'orderId'
});
var Parcel = require('../parcel/Parcel');
OrderParcel.belongsTo(Parcel, {
as: 'Parcel',
foreignKey: 'parcelId'
});

One way is to use sequelize.query:
As there are often use cases in which it is just easier to execute raw
/ already prepared SQL queries, you can utilize the function
sequelize.query.
var query = "SELECT count(p.*) FROM parcels as p" +
" LEFT JOIN orders_parcels as op ON op."parcelId" = p.id" +
" LEFT JOIN orders as o ON op."orderId" = o.id" +
" WHERE o.customerId=1;";
sequelize.query(query, { type: sequelize.QueryTypes.SELECT}).success(function(count){
console.log(count); // It's show the result of query
res.end();
}).catch(function(error){
res.send('server-error', {error: error});
});
Raw Queries docs

Assuming that you've defined the associations, you can use Model.findAndCountAll. It'd look something like this:
Parcel.findAndCountAll({
include: [{
model: OrderParcel,
required: true,
include: [{
model: Order,
where: {
customerId: idNum
}
}]
}]
}).then(function(result) {
});

I totally agree with Evan Siroky's approach yet the code has to be simplified to work properly:
Parcel.findAndCountAll({
include: [{
model: Order,
where: {
customerId: idNum
},
duplicating: false // Add this line for retrieving all objects
}]
}).then(function(result) {
console.log('Rows: ' + result.rows + ' Count: ' + result.count)
});
Remember to connect your models with belongsToMany method!

Related

Sequelize getters do not run on model.findall( { raw:true } )

I have a photo attribute in one of my Sequelize models. I have a getter for it.
photo: {
type: Sequelize.STRING,
get: function () {
if (this.getDataValue("photo"))
return process.env.IMAGE_PREFIX + this.getDataValue("photo")
else
return this.getDataValue("photo")
}
},
but when i want to findall records in this model with ({raw:true}) getter doesn't work.
const categories = await Category.findAll({
limit: parseInt(limit),
offset: parseInt(offset),
raw: true,
where: {
title: { [Op.like]: '%' + searchString + '%' }
}
});
if i use ({plain:true}) instead , just finds one record.
Due to newer version of sequelize as documentation itself says:
Getters wont run with instance.get({ raw: true }), use instance.get({ plain: true })
You can read more about it here

Sequelize - "array_agg" in a "having clause"

I have the following situation:
Table computers, table flags and table computerFlags that links them.
Table computerFlags looks like:
computerName | flagId
computer1 | flag1
computer1 | flag2
computer2 | flag2
computer2 | flag3
computer3 does not have any flags, so it is not in the table
I'm working with sequelize, and I'm trying to execute a query to count all computers without "flag3", in the above example table the answer would be 2 ("computer1" and "computer3").
This is the code I have so far:
import { DataTypes, Sequelize, Op } from "sequelize";
(async () => {
try {
const postgresDB = new Sequelize('postgres://<my-pg-credentials>>', {
logging: true,
});
const computers = postgresDB.define('computer', {
name: {
type: DataTypes.TEXT,
primaryKey: true
},
// Other computer fields
});
const flags = postgresDB.define('flag', {
id: {
type: DataTypes.TEXT,
primaryKey: true
},
name: DataTypes.TEXT,
});
const computerFlags = postgresDB.define('computerFlag', {}, { createdAt: false, updatedAt: false })
computers.belongsToMany(flags, { through: computerFlags });
flags.belongsToMany(computers, { through: computerFlags });
await postgresDB.sync({ alter: true })
const c = await computers.count({
distinct: true,
group: ['computer.name'],
include: [
{
model: flags
},
]
});
} catch (err) { console.log(err) }
})();
I get a half-good result with the next SQL query:
select count("computerName") from "computerFlags"
group by "computerName"
having '2' != all(array_agg("flagId"))
But I can't find a way to produce this in sequelize, and also, for the above table it'll return 1 as 'computer3' is not in the table
To execute this with sequelize, I would like to do something like that:
having: {
[[Sequelize.fn('array_agg', Sequelize.col('flag.id')), 'flagIds']] : {
[Op.all]: {
[Op.ne]: '2'
}
}
}
But there are two problems with that:
I cannot use [[Sequelize.fn ...]] as a left operand
I'm not sure if the way I refer to the flag ID is correct as it should be something like computer->flags->flagId. computer->flags returns an array of flags when I use findAll, each one containing flagId.
I'm really lost and confuse and I'd appreciate your help.
I'm trying to execute a query to count all computers without "flag3"
So use NOT EXISTS. (Some would say, a "semi-anti-join".)
SELECT count(*)
FROM computers c
WHERE NOT EXISTS (
SELECT FROM computerFlags cf
WHERE cf.computerName = c.computerName
AND cf.flagId = 'flag3'
);
Only eliminates computers from the count that actually do have an entry with 'flag3'.
Should perform best.
Aside: CaMeL case names are not ideal for Postgres. See:
Are PostgreSQL column names case-sensitive?

Sequelise creates weird query

I have 3 tables city, car, images
I need to get cars of cities for city/parent_id = 21 and images of the cars as well. (using a where clause to filter images on bases of sort_number)
My raw query is this:
SELECT
`car`.`car_id`,
`car`.`chassis_no`,
`images`.`car_image_id` AS `images.car_image_id`,
`images`.`directory` AS `images.directory`,
`images`.`file_name` AS `images.file_name`
FROM `car` AS `car`
INNER JOIN `city` AS `city` ON
`car`.`parent_id` = `city`.`id`
AND `city`.`parent_id` = 21
INNER JOIN `images` AS `images` ON
`car`.`car_id` = `images`.`car_id`
AND `images`.`sort_number` = 1
limit 0, 5;
and the Sequelise generated query is this:
explain SELECT
`car` .*,
`images`.`car_image_id` AS `images.car_image_id`,
`images`.`directory` AS `images.directory`,
`images`.`file_name` AS `images.file_name`
FROM
(
SELECT
`car`.`car_id`,
`car`.`chassis_no`
FROM
`car` AS `car`
INNER JOIN `city` AS `city` ON
`car`.`parent_id` = `city`.`id`
AND `city`.`parent_id` = 21
WHERE
(
SELECT
`car_id`
FROM
`images` AS `images`
WHERE
(`images`.`sort_number` = 1
AND `images`.`car_id` = `car`.`car_id`)
LIMIT 1 ) IS NOT NULL
LIMIT 0,
5) AS `car`
INNER JOIN `images` AS `images` ON
`car`.`car_id` = `images`.`car_id`
AND `images`.`sort_number` = 1;
Doing explain shows the sequelise query is not very efficient and it does looks weird to me, instead of plane joins. The result of the both queries are same though.
There are my relations:
//Relations
city.hasMany(car, { foreignKey: 'parent_id', sourceKey: 'id' })
car.belongsTo(city, {
foreignKey: 'parent_id',
sourceKey: 'id'
})
car.hasMany(image, { foreignKey: 'car_id', sourceKey: 'car_id' })
image.belongsTo(car, { foreignKey: 'car_id', sourceKey: 'car_id' })
The query code as follows:
return this.findAll({
attributes: ['car_id', 'car_model'],
include: [
{
model: db.city,
required: true,
attributes: [],
where: { parent_id: 21 }
},
{
model: db.image,
required: true,
attributes: ['image_id', 'file_name'],
where: { sort_number: 1 }
}
],
offset: 0,
limit: 5
})
Is there any way to make it like the raw query join I have above? What am I missing?
Is Sequelise limmited in generating better queries?
Should I use raw query function to write my joins myself it can't be helped?
Please guide, I'm new to sequelise. sorry for long post.

Set field in mongoose document to array length

I have a Mongoose document (Mongoose 5.4.13, mongoDB 4.0.12):
var SkillSchema = new mongoose.Schema({
skill: { type: String },
count: { type: Number, default: 0 },
associatedUsers: [{ type : mongoose.Schema.Types.ObjectId, ref: 'User' }]
});
That I update as follows:
var query = { skill: req.body.skill };
var update = { $addToSet: { associatedUsers: req.params.id } };
var options = { upsert: true, new: true, setDefaultsOnInsert: true };
await skillSchema.findOneAndUpdate(query, update, options);
During this update, I would like to also update count to be equal to the length of associatedUsers.
Ideally I want this to happen at the same time as updating the other fields (i.e not in a subsequent update), either via a pre-hook or within findOneAndUpdate.
I've tried using a pre hook after schema definition:
SkillSchema.pre('findOneAndUpdate', async function(){
console.log("counting associated users");
this.count = this.associatedUsers.length;
next();
});
As well as using aggregate in my UPDATE route:
await skillSchema.aggregate([{ $project: { count: { $size: "$associatedUsers" } } } ])
But I can't get either to work.
Does anyone have any suggestions for how I could achieve this?
You could use $set like this in 4.2 which supports aggregation pipeline in update.
The first $set stage calculates a associatedUsers based on the previous and new value. $setUnion to keep the distinct associatedUsers values.
The second $set stage calculates tally based on the associatedUsers calculated in the previous stage.$size to calculate the length of associatedUsers values.
var query = {skill: req.body.skill};
var update = [{ $set: { "associatedUsers":{"$setUnion":[{"$ifNull":["$associatedUsers",[]]}, [req.params.id]] }}}, {$set:{tally:{ $size: "$associatedUsers" }}}];
var options = { upsert: true, new: true, setDefaultsOnInsert: true };
await skillSchema.findOneAndUpdate(query, update, options)
If any argument resolves to a value of null or refers to a field that is missing, $setUnion returns null. So just needed to safeguard our operation with $ifNull
About tally and associatedUsers.length
// define your schema object
var schemaObj = {
skill: { type: String },
associatedUsers: { type: Array }
};
// get the length of users
var lengthOfAsUsers = schemaObj.associatedUsers.length;
// add tally to schema object and set default to the length of users
schemaObj.tally = { type: Number, default: lengthOfAsUsers };
// and pass your schema object to mongoose.Schema
var SkillSchema = new mongoose.Schema(schemaObj);
module.exports = SkillSchema;
EDIT
you can update tally subsequently, but recommended solution would be to use this method
https://mongoosejs.com/docs/populate.html
const id = "nameSomeId";
SkillSchema.find({ _id: id }).then(resp => {
const tallyToUpdate = resp.associatedUsers.length;
SkillSchema.findOneAndUpdate({ _id: id }, { tally: tallyToUpdate }).then(
resp => {
console.log(resp);
}
);
});
The solution I have will only work on mongodb v 4.2 as it has option to use aggregate in the update and will only need one query as:
skillSchemafindOneAndUpdate(
{skill:"art"},
[
{ $set: {
associatedUsers:{
$cond:{
if: {$gte: [{$indexOfArray: ["$associatedUsers", mongoose.Types.ObjectId(req.params.id)]}, 0]},
then: "$associatedUsers",
else: { $cond:{
if: { $isArray: "$associatedUsers" },
then: {$concatArrays:["$associatedUsers",[mongoose.Types.ObjectId(req.params.id)]]},
else: [mongoose.Types.ObjectId(req.params.id)]
}}
}
}}},
{$set:{
associatedUsers:"$associatedUsers",
tally:{$size:"$associatedUsers"},
}}
],
{upsert:true,new:true}
)
ref: https://docs.mongodb.com/manual/reference/method/db.collection.update/#update-with-aggregation-pipeline
The "Group" field does not appear in the schema. On MongoDB Shell, these codes will work.
However, Mongoose will also give an error because the schema is validated.
Is the "Group" field a dynamic field? I think the problem with the schema will be solved.
var mongoose = require("mongoose");
var SkillSchema = new mongoose.Schema({
skill: { type: String },
tally: { type: Number, default: 0 },
associatedUsers: { type: Array },
group: { type: Array }
});

Custom or override join in Sequelize.js

I need to create a custom join condition using Sequelize.js with MSSQL. Specifically, I need to join TableB based on a COALESCE value from columns in TableA and TableB and end up with a join condition like this:
LEFT OUTER JOIN [TableB]
ON [TableB].[ColumnB] = COALESCE (
[TableC].[ColumnC],
[TableA].[ColumnA]
)
I'd settle for an OR clause in my join:
LEFT OUTER JOIN [TableB]
ON [TableB].[ColumnB] = [TableA].[ColumnA]
OR [TableB].[ColumnB] = [TableC].[ColumnC]
I read that you can achieve behaviour like this by including required: false in your scope definition. As you can see, I've plastered my scope with it attempting to get this to work.
The best I could get is this (note the AND clause):
LEFT OUTER JOIN [TableB]
ON [TableB].[ColumnB] = [TableA].[ColumnA]
AND [TableB].[ColumnB] = COALESCE (
[TableC].[ColumnC],
[TableA].[ColumnA]
)
If I were using MySQL, I think I could simply use the COALESCE value from the SELECT in the JOIN and be good to go but in my prior research read that it was required to recalculate the value.
I've included a stripped down model definition for TableA:
export default (sequelize, DataTypes) => sequelize.define('TableA',
{
// Attributes omitted..
},
{
classMethods: {
associate ({
TableB,
TableC
}) {
this.belongsTo(TableB, {
foreignKey: 'ColumnA',
targetKey: 'ColumnB'
});
this.belongsTo(TableC, {
foreignKey: 'ColumnA',
targetKey: 'ColumnC'
});
},
attachScope ({
TableB,
TableC
}) {
this.addScope('defaultScope', {
attributes: [
...Object.keys(this.attributes),
[
sequelize.fn(
'COALESCE',
sequelize.col('[TableC].[ColumnC]'),
sequelize.col('[TableA].[ColumnA]')
),
'ColumnA'
]
],
include: [
{
model: TableB,
where: {
ColumnB: sequelize.fn(
'COALESCE',
sequelize.col('[TableC].[ColumnC]'),
sequelize.col('[TableA].[ColumnA]')
)
},
required: false
},
{
model: TableC,
required: false
}
],
required: false
}, { override: true });
}
}
}
);
Any assistance would be greatly appreciated, and if any additional information is required please let me know.
Note: I'm working with a legacy database and unfortunately cannot change the data structure.
Hi I had the same issue and finally I solved using a SQL pure query.
Example:
const query = `SELECT s.*, us.UserId \
FROM UserSections AS us \
RIGHT JOIN Sections AS s ON (s.id = us.SectionId) \
WHERE s.parentId = ${sectionId}`;
return db.query(query, { type: db.QueryTypes.SELECT });
The problem is that this function will return a IMyType instead of IMyTypeInstance
Can it works for you??

Categories

Resources