Access inner join data from sequelize using include - node.js

I am trying to join two tables using Sequelize's include as such:
models.user.findAll({include: {model: models.boardMember, required:true}})
.then(function(board) {
console.log(board);
res.render('contact', { title: 'Kontakt', board: board });
});
My models look like this using the sequelize express example:
User (it doesn't quite feel right having the hasMany here instead of in boardMembers, but I didn't want to have the foreign key field in the users table)
module.exports = function(sequelize, DataTypes) {
var user = sequelize.define('user', {
//lots of normal user fields(username, password, access...
}, {
classMethods: {
associate: function(models) {
user.hasMany(models.boardMember, {
foreignKey: {
allowNull: false
}
});
}
}
});
return user;
};
boardMember
module.exports = function(sequelize, DataTypes) {
var boardMember = sequelize.define('boardMember', {
post: {
type: DataTypes.STRING,
unique: false,
allowNull: false
}
});
return boardMember;
};
I then want to access the data returned in a table using handlebars:
{{#each board}}
<tr>
<td>{{boardMembers.post}}</td>
<td>{{firstName}} {{surName}}</td>
<td>{{email}}</td>
</tr>
{{/each}}
Here is where I get it wrong(I think). The names and email appear, but not the post. I've tried using only post as well but to no avail. This is odd I think because the query that is generated looks like this (I removed the createdAt and updatedAt columns to make it shorter for you to read):
SELECT
`user`.`id`, `user`.`username`, `user`.`password`,
`user`.`firstName`, `user`.`surName`, `user`.`email`, `user`.`access`,
`boardMembers`.`id` AS `boardMembers.id`, `boardMembers`.`post` AS `boardMembers.post`,
`boardMembers`.`userId` AS `boardMembers.userId`
FROM
`users` AS `user`
INNER JOIN
`boardMembers` AS `boardMembers` ON `user`.`id` = `boardMembers`.`userId`;
The console.log outputs something like this(the data obviously changed):
[ Instance {
dataValues:
{ id: 2,
username: 'username',
password: 'hashedPassword',
firstName: 'User',
surName: 'Name',
email: 'user#name.com',
access: '0',
boardMembers: [Object] },
...
]
Any help is much appreciated!
Thanks,
Freece

Per the sequelize documentation the value for the include attribute is a list.
You can try with the special value include: [{ all: true, nested: true }] and see if it works for you.
Additionally you have a problem with your template becuase you have stablished a one to many relation and therefore the boardMemebers attribute of model instances are arrays.

Related

Nested associated data through Sequelize join tables

Using Sequelize, I'm trying to get an output like this:
[{
"Id": 1,
"Name": "Game 1",
"Teams": [{
"Id": 1,
"Name": "Team 1",
"Users": [{
"Id": 1,
"UserName": "User 1"
}]
}]
}, {
"Id": 2,
"Name": "Game 2",
"Teams": [{
"Id": 1,
"Name": "Team 1",
"Users": [{
"Id": 2,
"UserName": "User 2"
}]
}]
}]
Note that Team 1 has 2 different users, but that's only because they're set up that way per game... so a user isn't tied directly to a team, but rather through a team game constraint. Basically, my Game HasMany Teams, and my Game/Team HasMany Users... a many-to-many-to-many relationship. I was trying to follow this thread, but it seems like what they're doing there doesn't actually work, as I tried doing this:
// models/Game.js
module.exports = (sequelize, types) => {
const GameModel = sequelize.define('Game', {
Id: {
type: types.INTEGER,
primaryKey: true,
autoIncrement: true
},
Name: {
type: types.STRING,
allowNull: false
}
});
GameModel.associate = (models) => {
GameModel.belongsToMany(models.Team, {
as: 'Teams',
foreignKey: 'GameId',
through: models.GameTeam
});
};
return GameModel;
};
// models/Team.js
module.exports = (sequelize, types) => {
const TeamModel = sequelize.define('Team', {
Id: {
type: types.INTEGER,
primaryKey: true,
autoIncrement: true
},
Name: {
type: types.STRING,
allowNull: false
}
});
TeamModel.associate = (models) => {
TeamModel.belongsToMany(models.Game, {
as: 'Games',
foreignKey: 'TeamId',
through: models.GameTeam
});
};
return TeamModel;
};
// models/User.js
module.exports = (sequelize, types) => {
const UserModel = sequelize.define('User', {
Id: {
type: types.INTEGER,
primaryKey: true,
autoIncrement: true
},
UserName: {
type: types.STRING,
allowNull: false
}
});
return UserModel;
};
// models/GameTeam.js
module.exports = (sequelize, types) => {
const GameTeamModel = sequelize.define('GameTeam', {
Id: {
type: types.INTEGER,
primaryKey: true,
autoIncrement: true
}
});
GameTeamModel.associate = (models) => {
GameTeamModel.belongsToMany(models.User, {
as: 'Users',
through: 'GameTeamUser'
});
};
return GameTeamModel;
};
The above models create the tables just fine, with what appears to be the appropriate columns. I then do some inserts and try to use a findAll on the Game model like this:
GameModel.findAll({
include: [{
association: GameModel.associations.Teams,
include: [{
association: GameTeamModel.associations.Users,
through: {
attributes: []
}
}],
through: {
attributes: []
}
}]
});
The query starts to go wrong at the 2nd include with the association of the Users. Because I'm trying to nest the users inside of the teams, I figured the join would attempt to use the unique ID on the through table (GameTeams.Id), but instead, the query ends up using this:
LEFT OUTER JOIN `GameTeamUser` AS `Teams->Users->GameTeamUser` ON `Teams`.`Id` = `Teams->Users->GameTeamUser`.`GameTeamId`
I figured the ON would be GameTeams.Id = Teams->Users->GameTeamuser.GameTeamId, but I don't know why it's not, and how to adjust it... I've tried using a custom on in my include (per the docs), but it seems to be ignored completely. Anyone have any advice? Or possibly a better way of structuring this, so it works the way I want it to?
I think you are overcomplicating this thinking you have a many to many to many..and i can see that the fields for your model for GameTeam do not match up with the foreign keys you have declared in your other models...
What do your database tables look like?
Am i correct in saying, that a game has many teams, and a team has many users... however a user can only be on one team at a time, and a team is only in one game at a time? (i am assuming the game/team join and the team/user join are simply temporary records in the join tables disappearing after the game is over etc)

Sequelize: Joining Many to Many tag table with another table in postgresql

I am trying to setup many to many relationship in Sequelize for my Postgres tables using a tag (lookup) table. I was successful in setting up the relationship.
Now my lookup table, has an additional field which is a foreign key to another table. I want the data for that table to be included in my result set. This is where I am having hard time figuring out the syntax.
My models:
Models.user = sequelize.define('user', { //attributes});
Models.school = sequelize.define('school', { //attributes });
Models.role = sequelize.define('role', { //attributes });
Models.userSchools = sequelize.define('user_school', {
user_school_id: { type: Sequelize.INTEGER, primaryKey: true }
});
Models.user.belongsToMany(Models.school, { through: Models.userSchools, foreignKey: 'user_id', otherKey: 'school_id' });
Models.school.belongsToMany(Models.user, { through: Models.userSchools, foreignKey: 'school_id', otherKey: 'user_id' });
Models.userSchools.belongsTo(Models.role, { foreignKey: 'role_id' });
Working query: This query gives me all the users and their schools.
Models.user.findAll({
where: {
//condition
},
include: [Models.school]
})
Now I want to include the role for each schools. None of the below queries are working.
Models.user.findAll({
where: {
//conditions
},
include: [Models.schools, Models.role]
})
Above query is throwing an error saying role is not associated to user, which is expected.
Models.user.findAll({
where: {
//conditions
},
include: [Models.schools, {model: Models.userSchools, include: Models.role}]
})
This query is also throwing an error:
"userSchools is not associated to user".
Can any one help with syntax to how I can get the role information from the userSchools model?
Edit:
I have add a "through" condition which gets me the role_id but not the related role data.
Models.user.findAll({
where: {
//conditions
},
include: [{
model: Models.school,
as: 'schools',
through: {
attributes: ['role_id']
}
}]
})

Sequelize, Issue with filtering on the join model for belongsToMany associations

The Sequelize docs suggest that you can filter your query on join table attributes using the following params on the options object:
[options.include[].through.where]
I've tried to use this formulation in the code below and found that the filtering does not work.
Model User and model Network are associated through the join table network-affiliations, which has additional attribute (boolean) 'confirmed'. I can't seem to write a query that returns only confirmed networks associated with a user.
My code is excerpted below.
const network_affiliations = db.define('network_affiliations', {
networkEmail: { type: Sequelize.STRING },
confirmed: { type: Sequelize.BOOLEAN }
}, {
freezeTableName: true,
defaultScope: {
where: {
confirmed: true
}
},
});
// User belongs to many Networks and Networks belong to many Users (join table)
User.belongsToMany(Network, { through: network_affiliations });
Network.belongsToMany(User, { through: network_affiliations });
//querying to find one user, including their confirmed networks
User.findOne({
where: {
email: req.body.email
},
include: [{ model: Network, through: { network_affiliations: { where: { confirmed: true } } } }]
})
I expect this query to return a user instance with its associated networks -- but only networks where confirmed: true. Instead I'm getting all networks associated with the user (including confirmed: false and confirmed: undefined).
As you can also see in the above code, I tried setting a defaultScope for the join table ({confirmed: true}). This also appears not to do anything.
I've also tried a User.findAll query that is otherwise identical, and that also does not work.
What am I missing, or Sequelize just not working here?
Sequelize version: "^3.30.4"

Sequelize Nested Association with Two Tables

I have a scenario where I am trying to query a parent table (document) with two associated tables (reference & user) that do not have a relationship to each other, but do have a relationship with the parent table. In SQL, this query would look like such and correctly outputs the data I am looking for:
select *
from `document`
left join `user`
on `document`.`user_id` = `user`.`user_id`
left join `reference`
on `document`.`reference_id` = `reference`.`reference_id`
where `user`.`organization_id` = 1;
However, associations that are nested have to relate in hierarchical order in order for the query to work. Since the nested associations are not related to each other I get an association error. How can I avoid this error? Would required: false have any influence on this?
models.Document.findAll({
order: 'documentDate DESC',
include: [{
model: models.User,
where: { organizationId: req.user.organizationId },
include: [{
model: models.Reference,
}]
}],
})
Error:
Unhandled rejection Error: reference is not associated to user!
Associations:
Document:
associate: function(db) {
Document.belongsTo(db.User, {foreignKey: 'user_id'}),
Document.belongsTo(db.Reference, { foreignKey: 'reference_id'});;
}
Reference:
associate: function(db){
Reference.hasMany(db.Document, { foreignKey: 'reference_id' });
}
Should I just chain queries instead?
If you want to replicate your query (as closely as possibly) use the following query. Keep in mind that the where on the User include will only serve to remove matches on Document.user_id where the User.organization_id does not match, but the Document will still be returned. If you want to omit Documents where the User.organization_id does not match use required: true.
User <- Document -> Reference
models.Document.findAll({
// this is an array of includes, don't nest them
include: [{
model: models.User,
where: { organization_id: req.user.organization_id }, // <-- underscored HERE
required: true, // <-- JOIN to only return Documents where there is a matching User
},
{
model: models.Reference,
required: false, // <-- LEFT JOIN, return rows even if there is no match
}],
order: [['document_date', 'DESC']], // <-- underscored HERE, plus use correct format
});
The error is indicating that the User model is not associated to the Reference model, but there are only definitions for the Document and Reference models in your description. You are joining these tables in your query with the include option, so you have to make sure they are associated. You don't technically need the foreignKey here either, you are specifying the default values.
Add Reference->User association
associate: function(db) {
// belongsTo()? maybe another relationship depending on your data model
Reference.belongsTo(db.User, {foreignKey: 'user_id'});
Reference.hasMany(db.Document, { foreignKey: 'reference_id' });
}
It also looks like you probably set underscored: true in your model definitions, so your query should reflect this. Additionally if you want to perform a LEFT JOIN you need to specify required: false on the include, otherwise it is a regular JOIN and you will only get back rows with matches in the included model. You are also using the wrong order format, it should be an array of values, and to sort by model.document_date DESC you should use order: [['document_date', 'DESC']].
Proper query arguments
models.Document.findAll({
order: [['document_date', 'DESC']], // <-- underscored HERE, plus use correct format
include: [{
model: models.User,
where: { organization_id: req.user.organization_id }, // <-- underscored HERE
required: false, // <-- LEFT JOIN
include: [{
model: models.Reference,
required: false, // <-- LEFT JOIN
}]
}],
});
If you are still having trouble, try enabling logging by setting logging: console.log in your Sequelize connection, that will show you all the queries it is running in your console.
It seems to me that your problem might be the associations, trying to link back to your primary key on Documents instead of the columns 'user_id' and 'reference_id'. You didn't post the table attributes so I might have understood this wrong.
Association on documents are ok.
Documents
associate: function(db) {
Document.belongsTo(db.User, {foreignKey: 'user_id'}), //key in documents
Document.belongsTo(db.Reference, { foreignKey: 'reference_id'}); //key in documents
}
User
associate: function(db) {
User.belongsTo(db.Document, {
foreignKey: 'id', //Key in User
targetKey: 'user_id' //Key in Documents
}),
}
Reference
associate: function(db) {
Reference.belongsTo(db.Document, {
foreignKey: 'id', //Key in reference
targetKey: 'reference_id' //Key in Documents
}),
}
Also
For debbuging consider using logging so you can see the queries.
var sequelize = new Sequelize('database', 'username', 'password', {
logging: console.log
logging: function (str) {
// do your own logging
}
});

Create Association in Sequelize to do a Left Join

I want to do a left join using Sequelize that does the same as this SQL code.
SELECT * FROM events LEFT JOIN zones ON event_tz=zid WHERE event_creator_fk=116;
I have two tables: events and zones (with a list of time zones).
When querying for all the events that are created by a specific user, I also want to get the name of the time zone and other details about the TZ.
I have tried many combinations of solutions by reviewing sample code, other Stack Overflow questions and the documentation as best I can. The query always works, but doesn't do any joins. That is, it below code always returns the list of events, but no time zone data from the zones table. The generated SQL is correct, except it doesn't have the ...LEFT JOIN zones ON event_tz=zid... part.
The below code is wrong. See answers for details.
db.Event.findAll(
{ where: { event_creator_fk: someUserID } },
{ include: [{ model: db.Zone } ]}
);
If I understand correctly, adding associations between tables in sequelize results in an additional column from automatically being created. This is not what I want to do. I do not want Sequelize to modify the tables or database in any way. I want to setup my database and it's tables without Sequelize. Therefore, I am not calling sequelize.sync(). I don't know if there is away to setup associations the way I want.
Model Definitions
module.exports = function (sequelize, DataTypes) {
var Event = sequelize.define('Event', {
eid: {
type: DataTypes.INTEGER,
primaryKey: true
},
event_tz: {
type: DataTypes.INTEGER,
primaryKey: true,
references: "Zone",
referencesKey: "zid"
},
}, {
classMethods: {
associate: function (models) {
return models.Event.hasOne(models.Zone);
}
},
freezeTableName: true,
timestamps: false,
tableName: 'events'
}
);
return Event;
};
module.exports = function (sequelize, DataTypes) {
return sequelize.define('Zone', {
zid: {
type: DataTypes.INTEGER,
primaryKey: true
}
}, {
classMethods: {
associate: function (models) {
return models.Zone.belongsTo(models.Event);
}
},
freezeTableName: true,
timestamps: false,
tableName: 'zones'
});
};
Table Definitions
DROP TABLE IF EXISTS zones;
CREATE TABLE IF NOT EXISTS zones (
zid integer NOT NULL,
country_code character(2) NOT NULL,
zone_name text NOT NULL,
PRIMARY KEY (zid)
);
DROP TABLE IF EXISTS events;
CREATE TABLE IF NOT EXISTS events (
eid BIGSERIAL NOT NULL,
event_tz SERIAL NOT NULL,
PRIMARY KEY (eid),
FOREIGN KEY (event_tz)
REFERENCES zones(zid) MATCH FULL ON DELETE RESTRICT
);
You need to reverse the associations and tell sequelize about your foreign key. belongsTo means 'add the fk to this model':
models.Event.belongsTo(models.Zone, { foreignKey: 'event_tz');
models.Zone.hasOne(models.Event, { foreignKey: 'event_tz');
// or hasMany if this is 1:m
Part of the problem was that I was using the findAll method incorrectly. The query options where and include should have been included as part of the same object. The first parameter to findAll is an options parameter. See here for more details. The correct code should look like the following.
db.Event.findAll(
{
where: { event_creator_fk: someUserID },
include: [{ model: db.Zone } ]
},
);

Resources