Sequelize belongsTo and include - node.js

I am having a hard time making use of BelongsTo relationship in a query.
Here is the relationship:
models.Area.hasMany(models.Airport);
models.Airport.belongsTo(models.Area, {foreignKey: 'areaId', as: 'area'});
Now I try to run a query:
models.Airport
.findAll( {
where: { active: 1 },
include: { model: models.Area }
})
I get an error:
Error: area is not associated to airport!
What am I doing wrong?

I figured it out. I can't say I fully understand the problem, but here it is.
When creating the relationship, I use as: 'area'.
The result is, I need to add as: 'area' to the include statement:
models.Airport
.findAll( {
where: { active: 1 },
include: { model: models.Area, as: 'area' }
})
Once the two match, things go better.

Related

Sequelize where scope with an joined table

I have a belongs to many relationship with a joined table
courseTree.associate = function (models) {
models.courses.belongsToMany(models.courses, {
through: 'course_tree',
as: 'children',
foreignKey: 'parent_id',
otherKey: 'child_id'
});
};
Currently when I run a find I get all my courses back, even the courses that are children, this is expected behavior but I want to have another scope where I can request only the courses with children.
In my scope I have the following where:
scopes: {
parents: {
where: {
children: { [Op.not] : null }
}
}
}
But the Rest Api gives me the following output
"name": "GeneralError",
"message": "column courses.children does not exist",
"code": 500
In the documentation I can't find any way to do this, I tried sequelize.literal and the in operator but without success. Does anyone knows how this is done, I'm sure I'm missing something.
Solved this by using Sequelize.literal
courses.addScope('getParents', {
where: {
[Op.and]: Sequelize.literal('"courses"."id" NOT IN (SELECT "course_tree"."child_id" FROM "course_tree")')
}
})
Another problem I ran into was that the child was the same model as the parent, in which case the where is also applied on the child. I solved this by adding where: false in the include.

Joining same table multiple times with Sequelize

I have the following models:
const User = Sequelize.define('user', {
login: Sequelize.DataTypes.STRING,
password: Sequelize.DataTypes.STRING,
is_manager: Sequelize.DataTypes.BOOLEAN,
notes: Sequelize.DataTypes.STRING
});
const Bike = Sequelize.define('bike', {
model: Sequelize.DataTypes.STRING,
photo: Sequelize.DataTypes.BLOB,
color: Sequelize.DataTypes.STRING,
weight: Sequelize.DataTypes.FLOAT,
location: Sequelize.DataTypes.STRING,
is_available: Sequelize.DataTypes.BOOLEAN
});
const Rate = Sequelize.define('rate', {
rate: Sequelize.DataTypes.INTEGER
});
Rate.belongsTo(User);
User.hasMany(Rate);
Rate.belongsTo(Bike);
Bike.hasMany(Rate);
And I'd like to select bikes with their average rates, plus rates of the current user for each bike:
Bike.findAll({
attributes: {include: [[Sequelize.fn('AVG', Sequelize.col('rates.rate')), 'rate_avg']],
},
include: [{
model: Rate,
attributes: []
}, {
model: Rate,
attributes: ['rate'],
include: [{
model: User,
attributes: [],
where: {
login: req.user.login
}
}]
}],
group: Object.keys(Bike.rawAttributes).map(key => 'bike.' + key) // group by all fields of Bike model
})
It constructs the following query: SELECT [bike].[id], [bike].[model], [bike].[photo], [bike].[color], [bike].[weight], [bike].[location], [bike].[is_available], AVG([rates].[rate]) AS [rate_avg], [rates].[id] AS [rates.id], [rates].[rate] AS [rates.rate] FROM [bikes] AS [bike] LEFT OUTER JOIN [rates] AS [rates] ON [bike].[id] = [rates].[bikeId] LEFT OUTER JOIN ( [rates] AS [rates] INNER JOIN [users] AS [rates->user] ON [rates].[userId] = [rates->user].[id] AND [rates->user].[login] = N'user' ) ON [bike].[id] = [rates].[bikeId] GROUP BY [bike].[id], [bike].[model], [bike].[photo], [bike].[color], [bike].[weight], [bike].[location], [bike].[is_available];
And fails: SequelizeDatabaseError: The correlation name 'rates' is specified multiple times in a FROM clause.
How do I write the query right? I need Sequelize to assign another alias to the rates table used in the 2nd join (and add its columns to the GROUP BY clause, but that's the next step).
You can do multiple inner joins with same table by adding extra same association with that model but with a different alias that is as: 'alias1' , as: 'alias2' ,... - all this existing with the same model + same type of association.
Also posted this solution at github issue: https://github.com/sequelize/sequelize/issues/7754#issuecomment-783404779
E.g. for Chats that have many Receiver
Associations (Duplicating for as many needed)
Chat.hasMany(Receiver, {
// foreignKey: ...
as: 'chatReceiver',
});
Chat.hasMany(Receiver, {
// foreignKey: ...
as: 'chatReceiver2',
});
Now you are left to include associated model multiple times all with different alias so it does not gets overridden.
So you can use them in query as below:
Chat.findAll({
attributes: ["id"],
include: [{
required: true,
model: Receiver,
as: 'chatReceiver', // Alias 1
attributes: [],
where: { userID: 1 }, // condition 1
}, {
required: true,
model: Receiver,
as: 'chatReceiver2', // Alias 2
attributes: [],
where: { userID: 2 }, // condition 2 as needed
}]
});
Solution :
Bike.findAll({
attributes: {include: [[Sequelize.fn('AVG', Sequelize.col('rates.rate')), 'rate_avg']],
},
include: [{
model: Rate,
attributes: []
}, {
model: Rate,
required : false , // 1. just to make sure not making inner join
separate : true , // 2. will run query separately , so your issue will be solved of multiple times
attributes: ['rate'],
include: [{
model: User,
attributes: [],
where: {
login: req.user.login
}
}]
group : [] // 3. <------- This needs to be managed , so please check errors and add fields as per error
}],
group: Object.keys(Bike.rawAttributes).map(key => 'bike.' + key) // group by all fields of Bike model
})
NOTE : READ THE COMMENTS
Sequelize doesn't support including through the same association twice (see here, here, and here). At the model level, you can define 2 different associations between Bike and Rate, but having to change the model, adding new foreign keys etc, is a very hacky solution.
Incidentally, it wouldn't solve your other problem, which is that you're grouping only by Bike but then want to select the user's rate. To fix that, you'd also have to change your grouping to include the user rates. (Note that if a user has more than 1 rate per bike, that might also create some inefficiency, as the rates for the bike are averaged repeatedly for each of the user's rates.)
A proper solution would be using window functions, first averaging the rates per bike and then filtering out all the rates not belonging to the logged in user. Might look something like this:
SELECT *
FROM (
SELECT bike.*,
users.login AS user_login,
AVG (rates.rate) OVER (PARTITION BY bike.id) AS rate_avg
FROM bike
INNER JOIN rates ON rates.bikeId = bike.id
INNER JOIN users ON rates.userId = users.id
)
WHERE user_login = :req_user_login
Unfortunately, as far as I'm aware sequelize doesn't currently support subqueries in the FROM clause and using window functions in this way, so you'd have to fall back to a raw query.

SequelizeEagerLoadingError when relationship between models has already been defined

I have an exports file that includes all the sequelize-models and then defines the relationship among the models. It looks something like:
// Snippet from the global init file
for (let modelFile of modelFileList) {
// ... Some code ...
// Require the file
appliedModels[modelName] = require(`../${MODEL_DIR}/${modelFile}`).call(null, _mysql);
}
//Define the relationship between the sql models
_defineRelationship(appliedModels);
function _defineRelationship(models) {
models._planAllocationModel.belongsTo(models._subscriptionModel, {
foreignKey: 'subscription_id',
targetKey: 'subscription_id'
});
}
But when I try to include the model like:
_subscriptionModel.findAll({
where: {
start_date: {
_lte: today // Get all subscriptions where start_date <= today
}
},
limit,
include: [
{
model: _planAllocationModel
}
]
});
There is an error thrown by sequelize: SequelizeEagerLoadingError: tbl_plan_allocation is not associated to tbl_subscription_info! What could be the reason for this? I have already initialized the relationshipt between the 2 models.
I was able to solve the problem. The relationship was defined as belongsTo which had to be changed to hasOne because of the type of join applied in the findAll query.

Sequelize one-to-many query (with include) produces Y is not associated to X

so i'm having problems with sequelize's one to many relationship, my associations are defined like this:
X.hasMany(Y, { as: 'Ys' });
Y.belongsTo(X, { as: 'X' });
and my findAll is here:
return X.findAll(
{
where: {
something: something,
},
include: [{ model: db.Y, as: 'Ys' }]
}
);
and this is producing the error:
"error": "Y (Ys) is not associated to X!"
Not quite sure what i'm doing wrong here :/
There's some confusion on your associations
logically 'X' has many 'Ys', association should be X.hasMany(Y, {as: 'Ys'});
'Y' belongs to 'X' should be Y.hasMany(X, {as: 'X'});

How to configure hasMany relationship with many-to-many table

I have the following setup:
Organization (1)----(*) OrganizationArticleItemMap (*)----(1) ArticleItem
(1)
|
|
|
(*)
ArticleItemPriceRule
An article item can thus belong to many organizations and an organization can have many article items. For every article item in an organization there will be multiple price rules.
The many to many relationship has been configured as such:
this.models.ArticleItem.belongsToMany(this.models.Organization, {
through: this.models.OrganizationArticleItemMap,
foreignKey: 'ArticleItemId'
});
this.models.Organization.belongsToMany(this.models.ArticleItem, {
through: this.models.OrganizationArticleItemMap,
foreignKey: 'OrganizationId'
});
I do not really know how I should configure ArticleItemPriceRule so that I can fetch the Price rules for all articles for a given organization.
I have tried the following :
this.models.ArticleItem.hasMany(this.models.OrganizationArticleItemMap, {
foreignKey: 'ArticleItemId',
as: 'OrganizationMaps'
});
this.models.OrganizationArticleItemMap.hasMany(this.models.ArticleItemPriceRule, {
as: 'Prices',
foreignKey: 'Organizations_articleitem_map_Id'
});
and then the following query:
DataAccess.dataContext.models.ArticleItem.findAll({
include: [
{
model: DataAccess.dataContext.models.Organization,
where: {OrganizationId: '1'},
attributes: [],
through: {
attributes: []
}
},
{
model: DataAccess.dataContext.models.OrganizationArticleItemMap,
as: 'OrganizationMaps',
required: true,
include: [{
model: DataAccess.dataContext.models.ArticleItemPriceRule,
as: 'Prices',
required: false
}]
}
],
where: {ArticleItemId: '1'}
})
The problem with this is that the sql query that was generated included an inner join with OrganizationArticleItemMap twice.
SELECT `ArticleItem`.`ArticleItemId`,
`ArticleItem`.`ArticleCategoryId`,
`ArticleItem`.`VisibleOnOnlineBooking`,
`OrganizationMaps`.`Organization_articleitem_map_Id` AS `OrganizationMaps.OrganizationArticleItemMapId`,
`OrganizationMaps`.`OrganizationId` AS `OrganizationMaps.OrganizationId`,
`OrganizationMaps`.`ArticleItemId` AS `OrganizationMaps.ArticleItemId`,
`OrganizationMaps.Prices`.`ArticleItemPriceRuleId` AS `OrganizationMaps.Prices.ArticleItemPriceRuleId`,
`OrganizationMaps.Prices`.`Organizations_articleitem_map_Id` AS `OrganizationMaps.Prices.Organizations_articleitem_map_Id`,
`OrganizationMaps.Prices`.`CurrencyId` AS `OrganizationMaps.Prices.CurrencyId`,
`OrganizationMaps.Prices`.`Price` AS `OrganizationMaps.Prices.Price`,
`OrganizationMaps.Prices`.`ValidFrom` AS `OrganizationMaps.Prices.ValidFrom`
FROM `articleitem` AS `ArticleItem`
INNER JOIN (`organization_articleitem_map` AS `Organizations.OrganizationArticleItemMap`
INNER JOIN `organizations` AS `Organizations` ON `Organizations`.`OrganizationId` = `Organizations.OrganizationArticleItemMap`.`OrganizationId`) ON `ArticleItem`.`ArticleItemId` = `Organizations.OrganizationArticleItemMap`.`ArticleItemId`
AND `Organizations`.`OrganizationId` = '1'
AND `Organizations`.`IsDeleted` = 0
INNER JOIN `organization_articleitem_map` AS `OrganizationMaps` ON `ArticleItem`.`ArticleItemId` = `OrganizationMaps`.`ArticleItemId`
LEFT OUTER JOIN `articleitempricerule` AS `OrganizationMaps.Prices` ON `OrganizationMaps`.`Organization_articleitem_map_Id` = `OrganizationMaps.Prices`.`Organizations_articleitem_map_Id`
AND `OrganizationMaps.Prices`.`IsDeleted` = 0
WHERE `ArticleItem`.`ArticleItemId` = '1'
AND `ArticleItem`.`IsDeleted` = 0;
Well, this is the response I got from the sequelize team on github:
Sequelize doesn't know that those two relations uses the same tables.
You look to be doing it correctly, we just aren't smart enough to
infer that we could use the join table just a single time.
https://github.com/sequelize/sequelize/issues/5376

Resources