Custom or override join in Sequelize.js - javascript

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??

Related

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?

Sequelize - Delete association records from parent instance

I am trying to find a way to remove rows from the DB through the parent model (menu) that has many children (foods). I only want to delete certain rows though, not all.
Menu.js
...
Menu.hasMany(models.Food, {
as: 'foods',
foreignKey: 'menuId',
sourceKey: 'id'
});
...
In my controller I have the following to try and delete certain foods off the menu.
...
const result = await menu.destroyFoods({
where: {
name: ['Pasta', 'Pizza']
}
});
...
I have also tried singular destroyFood as well. For both I am getting destoryFood/destoryFoods is not a function. Is there any easy way to do this from the instance of menu? New to sequelize, would love some help. Thanks.
Thanks
You can use menu.removeFoods() and menu.removeFood() - see Special methods/mixins added to instances: Foo.hasMany(Bar) for more information.
You will also need to use the Op.in query operator to specify multiple values for Food.name.
const { Op } = require('sequelize');
const result = await menu.removeFoods({
where: {
name: {
[Op.in]: ['Pasta', 'Pizza'],
}
}
});
This is the equivalent of calling Food.destroy() where the menuId is equal to the menu.id from the earlier result.
const results = await Food.destroy({
where: {
menuId: menu.id,
name: {
[Op.in]: ['Pasta', 'Pizza'],
},
},
});

Possible to WHERE on nested includes in Sequelize?

I've got a problem that I've been stuck on, to no avail - seemingly similar in nature to Where condition for joined table in Sequelize ORM, except that I'd like to query on a previous join. Perhaps code will explain my problem. Happy to provide any extra info.
Models:
A.hasMany(B);
B.belongsTo(A);
B.hasMany(C);
C.belongsTo(B);
This is what I'd like to be able to achieve with Sequelize:
SELECT *
FROM `A`AS `A`
LEFT OUTER JOIN `B` AS `B` ON `A`.`id` = `B`.`a_id`
LEFT OUTER JOIN `C` AS `B->C` ON `B`.`id` = `B->C`.`b_id`
AND (`B`.`b_columnName` = `B->C`.`c_columnName`);
This is how I imagine this working: (instead it will create a raw query (2 raw queries, for A-B/C) with AND ( `C`.`columnName` = '$B.columnName$')) on the join (second arg is a string). Have tried sequelize.col, sequelize.where(sequelize.col..., etc..)
A.findOne({
where: { id: myId },
include: [{
model: B,
include: [{
model: C,
where: { $C.c_columnName$: $B.b_columnName$ }
}]
}]
});
Use the Op.col query operator to find columns that match other columns in your query. If you are only joining a single table you can pass an object instead of an array to make it more concise.
const Op = Sequelize.Op;
const result = await A.findOne({
include: {
model: B,
include: {
model: C,
where: {
c_columnName: {
[Op.col]: 'B.b_columnName',
},
}
},
},
});

Filter to not Include Matched Values in Result Arrays

I have a query where I first want to match find the list of matched users and then filter the matches out from the array of external users that was passed in so that I am left with users Id's that have not been matched yet.
Here is a the match Schema:
const mongoose = require('mongoose'); // only match two users at a time.
const Schema = mongoose.Schema;
const MatchSchema = new Schema({
participants: [{
type: String, ref: 'user'
}],
blocked: {
type: Boolean,
default: false
}
});
Here is the query with explanations:
db.getCollection('match').aggregate([
{
'$match': {
'$and': [
{ participants: "599f14855e9fcf95d0fe11a7" }, // the current user.
{ participants: {'$in': [ "598461fcda5afa9e0d2a8a64","598461fcda5afa9e0d111111", "599f14855e9fcf95d0fe5555"] } } // array of external users that I want to check if the current user is matched with.
]
}
},
{
'$project': {
'participants': 1
}
},
This returns the following result:
{
"_id" : ObjectId("59c0d76e66dd407f5efe7112"),
"participants" : [
"599f14855e9fcf95d0fe11a7",
"599f14855e9fcf95d0fe5555"
]
},
{
"_id" : ObjectId("59c0d76e66dd407f5efe75ac"),
"participants" : [
"598461fcda5afa9e0d2a8a64",
"599f14855e9fcf95d0fe11a7"
]
}
what I want to do next it merge the participants array form both results into one array.
Then I want to take away the those matching items from the array of external users so that I am left with user id's that have not been matched yet.
Any help would be much appreciated!
If you don't want those results in the array, then $filter them out.
Either by building the conditions with $or ( aggregation logical version ):
var currentUser = "599f14855e9fcf95d0fe11a7",
matchingUsers = [
"598461fcda5afa9e0d2a8a64",
"598461fcda5afa9e0d111111",
"599f14855e9fcf95d0fe5555"
],
combined = [currentUser, ...matchingUsers];
db.getCollection('match').aggregate([
{ '$match': {
'participants': { '$eq': currentUser, '$in': matchingUsers }
}},
{ '$project': {
'participants': {
'$filter': {
'input': '$participants',
'as': 'p',
'cond': {
'$not': {
'$or': combined.map(c =>({ '$eq': [ c, '$$p' ] }))
}
}
}
}
}}
])
Or use $in ( again the aggregation version ) if you have MongoDB 3.4 which supports it:
db.getCollection('match').aggregate([
{ '$match': {
'participants': { '$eq': currentUser, '$in': matchingUsers }
}},
{ '$project': {
'participants': {
'$filter': {
'input': '$participants',
'as': 'p',
'cond': {
'$not': {
'$in': ['$p', combined ]
}
}
}
}
}}
])
It really does not matter. It's just the difference of using JavaScript to build the expression before the pipeline is sent or letting a supported pipeline operator do the array comparison where it is actually supported.
Note you can also write the $match a bit more efficiently by using an "implicit" form of $and, as is shown.
Also note you have a problem in your schema definition ( but not related to this particular query ). You cannot use a "ref" to another collection as String in one collection where it is going to be ObjectId ( the default for _id, and presumed of the hex values obtained ) in the other. This mismatch means .populate() or $lookup functions cannot work. So you really should correct the types.
Unrelated to this. But something you need to fix as a priority.

Node.js Sequelize ManyToMany relations producing incorrect SQL

I'm having a problem with sequelize ManyToMany relations.
Here are my models...
var db = {
players: sequelize.define('players', {
name: Sequelize.STRING
}),
teams: sequelize.define('teams', {
name: Sequelize.STRING
}),
init: function() {
this.players.hasMany(this.teams, {joinTableName: 'teams_has_players'});
this.teams.hasMany(this.players, {joinTableName: 'teams_has_players'});
this.players.sync();
this.teams.sync();
}
};
Here's the find
db.players.findAll({
where: {team_id: 1},
include: ['teams']
}).success(function(results) {
// print the results
});
The above find will produce the following SQL:
SELECT
players . *,
teams.name AS `teams.name`,
teams.id AS `teams.id`
FROM
players
LEFT OUTER JOIN
teams_has_players ON teams_has_players.player_id = players.id
LEFT OUTER JOIN
teams ON teams.id = teams_has_players.team_id
WHERE
players.team_id = '1';
What appears to be wrong with this is that the WHERE statement should be WHERE teams.team_id = '1'
Where am I going wrong with this?
Thanks in advance
Hmm actually everything looks quite OK-ish. db.players.findAll with where: { team_id: 1 } should create a query with WHERE players.team_id = '1'. That's perfectly expected. Also, you teams won't have a team_id but an id instead. However, there is a good chance that include is broken atm.

Categories

Resources