Sequelize join twice on two tables - node.js

I have two table: Conversations and ConversationParticipants.
I need to get the list of conversations that both users 1 and 2 participate too.
In MySQL, the query would be this:
SELECT conversation_participants.conversation_id FROM conversation_participants
JOIN conversations t1
ON t1.conversation_id = conversation_participants.conversation_id
AND conversation_participants.user_id = 11
JOIN conversation_participants t2
ON t1.conversation_id = t2.conversation_id
AND t2.user_id = 2
However, in Sequelize I cannot understand how to set models relations so that I can make a single query. I tried this without success (please note that this is almost pseudo code, reported as such for clarity):
var Conversation = sequelize.define('conversations', {
conversationId: Sequelize.INTEGER.UNSIGNED,
});
var ConversationParticipant = sequelize.define('conversation_participants', {
participationId: Sequelize.INTEGER.UNSIGNED,
userId: Sequelize.INTEGER.UNSIGNED,
conversationId : Sequelize.INTEGER.UNSIGNED,
});
Conversation.hasMany(ConversationParticipant, { as : 'Participants'});
and then
ConversationParticipant.findAll({
where : { userId : 1 },
include : [{
model : Conversation,
as : 'conversation',
include : [{
model : ConversationParticipant,
as : 'Participants',
where : { userId : 2 }
}]
}]
I get the following error:
Error: conversation_participants is not associated with conversations!.
Any idea?

One way you could do it is find the conversations that have participants with user 1 or 2, and afterwards filter the conversations that have both of them:
const userIdsFilter = [1,2];
//find Conversations that have user 1 OR user 2 as participants,
Conversation.findAll({
include : [{
model : ConversationParticipant,
as : 'conversation_participants',
where : { userId : { $in: userIdsFilter }}
}]
}).then((conversations) =>
{
//filter the conversations that have the same length participants
//as the userIdsFilter length (that way it excludes the ones
//that have just one of the users as participants)
return conversations.filter((conversation) =>
conversation.conversation_participants.length === userIdsFilter.length);
}).then((conversations) =>
{
//do what you need with conversations..
})

You are missing the belongsTo association in the ConversationParticipant definition. Sequelize model needs explicit association since you are trying to access Conversation via the ConversationParticipant instance even though Conversation is associated ConversationParticipant is not.
Something like this in addition to hasMany association:
ConversationParticipant.belongsTo(Conversation, { as : 'conversation',
foreignKey: 'conversationId', targetKey: 'conversationId'});
And then remove the conversationId field definition from ConversationParticipant model since belongsTo also creates that for you.

Related

How to filter through 3 table joins using sequelize in expressjs api

I have 3 tables named:
//restaurants
columns ( id, name, restaurant_type_id(FK)
//restaurant_branches
columns ( id, name, restaurant_id(FK)
//restaurant_types
columns ( id, restaurant_type_name('italian', 'french'...etc))
I would like to filter restaurant_branches by restaurant_type_id using query params in my restaurant_branches.findAll(); action in the controller as the following.
const findAll = async (req, res) => {
let RestaurantTypeId= req.query.restaurantType ? parseInt(req.query.restaurantType): null ;
var type = RestaurantTypeId ? {where:{ restaurantTypeId: RestaurantTypeId }} : null ;
console.log(RestaurantTypeId);
await RestaurantBranch.findAll({
order: [
['id', 'ASC']
],
include:
[
{
model: Restaurant,
type,
include: [{
model: RestaurantType,
}
]
}
]
}).then((restaurantBranches) => {
return res.status(200).send({
message: "restaurant branches returned",
data: restaurantBranches
})
})
.catch((error) => {res.status(500).send(error.message);});
}
module.exports = {
findAll
}
//Sequelize Associations
db.RestaurantType.hasMany(db.Restaurant);
db.Restaurant.belongsTo(db.RestaurantType);
// Restaurant / Restaurant Branches
db.RestaurantBranch.belongsTo(db.Restaurant);
db.Restaurant.hasMany(db.RestaurantBranch);
Sequelize log:
Executing (default): SELECT "restaurant_branches"."id", "restaurant_branches"."name", "restaurant_branches"."description", "restaurant_branches"."email", "restaurant_branches"."phoneNumber", "restaurant_branches"."address", "restaurant_branches"."country_code", "restaurant_branches"."image", "restaurant_branches"."latitude", "restaurant_branches"."longitude", "restaurant_branches"."workingHours", "restaurant_branches"."workingDays", "restaurant_branches"."offDays", "restaurant_branches"."locationAddress", "restaurant_branches"."locationCity", "restaurant_branches"."status", "restaurant_branches"."hasParking", "restaurant_branches"."instruction", "restaurant_branches"."isActive", "restaurant_branches"."isDeleted", "restaurant_branches"."createdAt", "restaurant_branches"."updatedAt", "restaurant_branches"."restaurantId", "restaurant_branches"."cityId", "restaurant_branches"."districtId", "city"."id" AS "city.id", "city"."name" AS "city.name", "city"."code" AS "city.code", "city"."status" AS "city.status", "city"."isDeleted" AS "city.isDeleted", "city"."createdAt" AS "city.createdAt", "city"."updatedAt" AS "city.updatedAt", "city"."countryId" AS "city.countryId", "district"."id" AS "district.id", "district"."name" AS "district.name", "district"."isDeleted" AS "district.isDeleted", "district"."createdAt" AS "district.createdAt", "district"."updatedAt" AS "district.updatedAt", "district"."cityId" AS "district.cityId", "restaurant"."id" AS "restaurant.id", "restaurant"."name" AS "restaurant.name", "restaurant"."aboutUs" AS "restaurant.aboutUs", "restaurant"."phoneNumber" AS "restaurant.phoneNumber", "restaurant"."address" AS "restaurant.address", "restaurant"."latitude" AS "restaurant.latitude", "restaurant"."longitude" AS "restaurant.longitude", "restaurant"."image" AS "restaurant.image", "restaurant"."countryCode" AS "restaurant.countryCode", "restaurant"."restaurantRegisterDocument" AS "restaurant.restaurantRegisterDocument", "restaurant"."isDeleted" AS "restaurant.isDeleted", "restaurant"."createdAt" AS "restaurant.createdAt", "restaurant"."updatedAt" AS "restaurant.updatedAt", "restaurant"."restaurantTypeId" AS "restaurant.restaurantTypeId", "restaurant"."categoryId" AS "restaurant.categoryId", "restaurant"."userId" AS "restaurant.userId", "restaurant->restaurant_type"."id" AS "restaurant.restaurant_type.id", "restaurant->restaurant_type"."name" AS "restaurant.restaurant_type.name", "restaurant->restaurant_type"."photo" AS "restaurant.restaurant_type.photo", "restaurant->restaurant_type"."createdAt" AS "restaurant.restaurant_type.createdAt", "restaurant->restaurant_type"."updatedAt" AS "restaurant.restaurant_type.updatedAt" FROM "restaurant_branches" AS "restaurant_branches" LEFT OUTER JOIN "cities" AS "city" ON "restaurant_branches"."cityId" = "city"."id" LEFT OUTER JOIN "districts" AS "district" ON "restaurant_branches"."districtId" = "district"."id" LEFT OUTER JOIN "restaurants" AS "restaurant" ON "restaurant_branches"."restaurantId" = "restaurant"."id" LEFT OUTER JOIN "restaurant_types" AS "restaurant->restaurant_type" ON "restaurant"."restaurantTypeId" = "restaurant->restaurant_type"."id" ORDER BY "restaurant_branches"."id" ASC;
so far I'm doing this, and I get All the restaurant branches if I GET request to this URL:
{{URL}}/restaurant_branches?restaurantType=2
what I'd like to be getting instead is all the restaurant branches whom their restaurants belong to the restaurant type with id 2
Any help or guidance is highly appreciated.
You included where condition in include option as a type prop that's why it does not work as expected. You just need to indicate where either with your condition or as an empty object:
var where = RestaurantTypeId ? { restaurantTypeId: RestaurantTypeId } : {} ;
include:
[
{
model: Restaurant,
where,
include: [{
model: RestaurantType,
}
]
}
]

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.

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

Sails.js - Get an object (model) using multiple join

I am new to node.js and newer to Sails.js framework.
I am currently trying to work with my database, I don't understand all the things with Sails.js but I manage to do what I want step by step. (I am used to some PHP MVC frameworks so it is not too difficult to understand the structure.)
Here I am trying to get a row from my database, using 2 JOIN clause. I managed to do this using SQL and the Model.query() function, but I would like to do this in a "cleaner" way.
So I have 3 tables in my database: meta, lang and meta_lang. It's quite simple and a picture being better than words, here are some screenshots.
meta
lang
meta_lang
What I want to do is to get the row in meta_table that match with 'default' meta and 'en' lang (for example).
Here are Meta and Lang models (I created them with sails generate model command and edited them with what I needed):
Meta
module.exports = {
attributes: {
code : { type: 'string' },
metaLangs:{
collection: 'MetaLang',
via: 'meta'
}
}
};
Lang
module.exports = {
attributes: {
code : { type: 'string' },
metaLangs:{
collection: 'MetaLang',
via: 'lang'
}
}
};
And here is my MetaLang model, with 3 functions I created to test several methods. The first function, findCurrent, works perfectly, but as you can see I had to write SQL. That is what I want to avoid if it is possible, I find it more clean (and I would like to use Sails.js tools as often as I can).
module.exports = {
tableName: 'meta_lang',
attributes: {
title : { type: 'string' },
description : { type: 'text' },
keywords : { type: 'string' },
meta:{
model:'Meta',
columnName: 'meta_id'
},
lang:{
model:'Lang',
columnName: 'lang_id'
}
},
findCurrent: function (metaCode, langCode) {
var query = 'SELECT ml.* FROM meta_lang ml INNER JOIN meta m ON m.id = ml.meta_id INNER JOIN lang l ON l.id = ml.lang_id WHERE m.code = ? AND l.code = ?';
MetaLang.query(query, [metaCode, langCode], function(err, metaLang) {
console.log('findCurrent');
if (err) return console.log(err);
console.log(metaLang);
// OK this works exactly as I want (I would have prefered a 'findOne' result, only 1 object instead of an array with 1 object in it, but I can do with it.)
});
},
findCurrentTest: function (metaCode, langCode) {
Meta.findByCode(metaCode).populate('metaLangs').exec(function(err, metaLang) {
console.log('findCurrentTest');
if (err) return console.log(err);
console.log(metaLang);
// I get what I expected (though not what I want): my meta + all metaLangs related to meta with code "default".
// What I want is to get ONE metaLang related to meta with code "default" AND lang with code "en".
});
},
findCurrentOthertest: function (metaCode, langCode) {
MetaLang.find().populate('meta', {where: {code:metaCode}}).populate('lang', {where: {code:langCode}}).exec(function(err, metaLang) {
console.log('findCurrentOthertest');
if (err) return console.log(err);
console.log(metaLang);
// Doesn't work as I wanted: it gets ALL the metaLang rows.
});
}
};
I also tried to first get my Meta by code, then my Lang by code, and MetaLang using Meta.id and Lang.id . But I would like to avoid 3 queries when I can have only one.
What I'm looking for would be something like MetaLang.find({meta.code:"default", lang.code:"en"}).
Hope you've got all needed details, just comment and ask for more if you don't.
Do you know what populate is for ? its for including the whole associated object when loading it from the database. Its practically the join you are trying to do, if all you need is row retrieval than quering the table without populate will make both functions you built work.
To me it looks like you are re-writing how Sails did the association. Id suggest giving the Associations docs another read in Sails documentation: Associations. As depending on your case you are just trying a one-to-many association with each table, you could avoid a middle table in my guess, but to decide better id need to understand your use-case.
When I saw the mySQL code it seemed to me you are still thinking in MySQL and PHP which takes time to convert from :) forcing the joins and middle tables yourself, redoing a lot of the stuff sails automated for you. I redone your example on 'disk' adapter and it worked perfectly. The whole point of WaterlineORM is to abstract the layer of going down to SQL unless absolutely necessary. Here is what I would do for your example, first without SQL just on a disk adapter id create the models :
// Lang.js
attributes: {
id :{ type: "Integer" , autoIncrement : true, primaryKey: true },
code :"string"
}
you see what i did redundantly here ? I did not really need the Id part as Sails does it for me. Just an example.
// Meta.js
attributes: {
code :"string"
}
better :) ?
// MetaLang.js
attributes:
{
title : "string",
desc : "string",
meta_id :
{
model : "meta",
},
lang_id :
{
model : "lang",
}
}
Now after simply creating the same values as your example i run sails console type :
MetaLang.find({meta_id : 1 ,lang_id:2}).exec(function(er,res){
console.log(res);
});
Output >>>
sails> [ { meta_id: 1,
lang_id: 2,
title: 'My blog',
id: 2 } ]
Now if you want to display what is meta with id 1 and what is lang with id 2, we use populate, but the referencing for join/search is just as simple as this.
sails> Meta_lang.find({meta_id : 1 ,lang_id:2}).populate('lang_id').populate('meta_id').exec(function(er,res){ console.log(res); });
undefined
sails> [ {
meta_id:
{ code: 'default',
id: 1 },
lang_id:
{ code: 'En',
id: 2 },
title: 'My blog',
id: 2 } ]
At this point, id switch adapters to MySQL and then create the MySQL tables with the same column names as above. Create the FK_constraints and voila.
Another strict policy you can add is to set up the 'via' and dominance on each model. you can read more about that in the Association documentation and it depends on the nature of association (many-to-many etc.)
To get the same result without knowing the Ids before-hand :
sails> Meta.findOne({code : "default"}).exec(function(err,needed_meta){
..... Lang.findOne({code : "En"}).exec(function(err_lang,needed_lang){
....... Meta_lang.find({meta_id : needed_meta.id , lang_id : needed_lang.id}).exec(function(err_all,result){
......... console.log(result);});
....... });
..... });
undefined
sails> [ { meta_id: 1,
lang_id: 2,
title: 'My blog',
id: 2 } ]
Have you tried:
findCurrentTest: function (metaCode, langCode) {
Meta.findByCode(metaCode).populate('metaLangs', {where: {code:langCode}}).exec(function(err, metaLang) {
console.log('findCurrentTest');
if (err) return console.log(err);
console.log(metaLang);
});
},

Resources