I have tables with columns as stated below,
TABLE A
COLUMN id
COLUMN b-id
TABLE B
COLUMN id
COLUMN c-id
TABLE C
COLUMN id
TABLE D
COLUMN id
COLUMN c-id
Now, I wanted to get all the D's for particular A.id
How to implement it using the sequelize models?
To be precise, I wanted to start with something like this
A.findOne({
where: { id }
})
BUT, do not want to make multiple sync calls one after one.
Thank you in advance :)
You would need to first define Models for each of the tables, and then set the associations between the Models. This will define the relationships that you can use in the query with the include property. Setting required: false will use a LEFT JOIN so that results are still returned from ModelA even when there is related row in ModelB, for example.
Associations
// ModelA - ModelB - ModelC < ModelD
ModelA.hasOne(ModelB);
ModelB.hasOne(ModelC);
ModelC.hasMany(ModelD, { as: 'modelDs' });
// ModelD > ModelC < ModelB < ModelA
ModelD.belongsTo(ModelC);
ModelC.hasMany(ModelB, { as: 'modelBs' });
ModelB.hasMany(ModelA, { as: 'modelAs' });
Query
const modelA = await ModelA.findByPk(id, {
include: {
model: ModelB,
include: {
model: ModelC,
include: {
model: ModelD,
as: 'modelDs',
required: false,
},
required: false,
},
required: false,
},
});
console.log(modelA.modelB.modelC.modelDs);
Related
I am using Sequelize with Express, and Node js and I am trying to define defaultScope for Models.
Card and Tag have a Many To Many association.
Here are Models definitions and addScope
// Models Associations
// ONE TO MANY
List.hasMany(Card, {
as: "cards",
});
Card.belongsTo(List, {
as: "list",
});
// MANY TO MANY
Card.belongsToMany(Tag, {
as: "tags",
through: "card_has_tag",
updatedAt: false,
});
Tag.belongsToMany(Card, {
as: "cards",
through: "card_has_tag",
updatedAt: false,
});
// SCOPES
Card.addScope("defaultScope", {
include: {
association: "tags",
},
});
List.addScope("defaultScope", {
include: {
association: "cards",
include: "tags",
},
});
// What I would like to implement
// If I comment lines below => List and Card queries are working
Tag.addScope("defaultScope", {
include: {
association: "cards",
},
});
I would like to print by default all related infos with associated relations.
I want to get this info when I execute a sequelize query for each model.
LISTS with associated :
cards
tags
CARDS with associated:
tags
TAGS with associated :
cards
I manage to get 1 & 2, but when I add Tag.addScopenothing is working anymore.
When I change defaultScope by another string by defining a scope all (for example) , and when I use model.scope("all").findAll(), this is working, but it is not what I would like to do becaue I want to use defaultScope to have a default behavior so I don't have to specify scope in queries command like (findAll...)
Is there a way I can do that ?
The way you are trying to set it up results in an endless recursion, you simply can't have it like that.
If you set it up like that and query Card it will include Tag which will include Card which will include Tag and so on until you get Maximum call stack size exceeded.
There is a workaround you can use, which is to add another scope which includes nothing, then specify that scope for the model in the defaultScope.
Tag.addScope("noInclude", {});
Card.addScope("noInclude", {});
Tag.addScope("defaultScope", {
include: [
{
model: Card.scope("noInclude"),
as: "cards"
}
]
});
Card.addScope("defaultScope", {
include: [
{
model: Tag.scope("noInclude"),
as: "cards"
}
]
});
This should give you the desired behaviour.
Issue explanation
I want to do a query with pagination that limits to 12 lines each query, to be more specific, that limits to 12 Batches each query. Actually the amount of lines get smaller because a belongsToMany association with a join table i got in this query. The join order to this query is: Offer > hasMany > Batch > belongsToMany > BatchFile > belongsTo > File. The problem is, when i have many registries in the File as 'gallery' association, it brings me duplicated registries of batch.
The query i'm trying to do
const { id } = req.params;
const { page = 1 } = req.query;
const offer = await Offer.findAndCountAll({
attributes: [ 'id', 'name', 'canceled_at'],
where: { id, canceled_at: null },
order: [['offer_batches', 'name', 'ASC']],
include: [
{
/* hasMany association */
model: Batch,
as: 'offer_batches',
attributes: [ 'id', 'name'],
include: [
{ /* Other includes... */ },
{
/* belongsToMany association */
model: File,
as: 'gallery',
attributes: ['id', 'path', 'url'],
},
],
},
],
subQuery: false,
limit: 12,
offset: (page - 1) * 12,
});
Model associations
Offer model
this.hasMany(Batch, { foreignKey: 'offer_id', as: 'offer_batches' });
Batch model
this.belongsToMany(File, { through: models.BatchFile, as: 'gallery' });
BatchFile model (join table)
this.belongsTo(Batch, { foreignKey: 'batch_id', as: 'batch' });
this.belongsTo(File, { foreignKey: 'file_id', as: 'file' });
What i already tried
Giving duplicating: false option to any included Model doesn't worked;
Giving separate: true to the Batch model doesn't worked too;
Giving required: true option to any included model doesn't worked too;
If i remove subQuery: false it doesn't respect the setted limit of lines, and i already tried with all of the above combinations;
I thought sequelize would deal with this situation without problems, maybe i'm doint something wrong.
If helps, here's the raw generated SQL:
SELECT
"Offer"."id",
"Offer"."name",
"Offer"."canceled_at",
"offer_batches"."id"
AS "offer_batches.id", "offer_batches"."name"
AS "offer_batches.name", "offer_batches->gallery"."id"
AS "offer_batches.gallery.id", "offer_batches->gallery"."path"
AS "offer_batches.gallery.path", "offer_batches->gallery->BatchFile"."created_at"
AS "offer_batches.gallery.BatchFile.createdAt", "offer_batches->gallery->BatchFile"."updated_at"
AS "offer_batches.gallery.BatchFile.updatedAt", "offer_batches->gallery->BatchFile"."file_id"
AS "offer_batches.gallery.BatchFile.FileId", "offer_batches->gallery->BatchFile"."batch_id"
AS "offer_batches.gallery.BatchFile.BatchId", "offer_batches->gallery->BatchFile"."batch_id"
AS "offer_batches.gallery.BatchFile.batch_id", "offer_batches->gallery->BatchFile"."file_id"
AS "offer_batches.gallery.BatchFile.file_id"
FROM "offer" AS "Offer"
LEFT OUTER JOIN "batch" AS "offer_batches" ON "Offer"."id" = "offer_batches"."offer_id"
LEFT OUTER JOIN (
"batch_file" AS "offer_batches->gallery->BatchFile"
INNER JOIN "file" AS "offer_batches->gallery"
ON "offer_batches->gallery"."id" = "offer_batches->gallery->BatchFile"."file_id"
)
ON "offer_batches"."id" = "offer_batches->gallery->BatchFile"."batch_id"
WHERE "Offer"."id" = '1' AND "Offer"."canceled_at" IS NULL
ORDER BY "offer_batches"."name"
ASC LIMIT 12 OFFSET 0;
Environment
Node: v14.18.0
package.json dependencies
pg: 8.7.1
pg-hstore: 2.3.4
sequelize: 6.9.0
Trying to find any solution on GitHub issues or stackoverflow, nothing solved this problem. Maybe i'm doing this query wrong, any help would be grateful and welcome :)
Well, i didn't found a quite solution for this, so i resolved to use Lazy loading for this situation instead of Eager loading, as mentioned on Sequelize Docs.
I splitted the query into two new queries.
First one, on the "master" model Offer, with a simple findOne():
const offer = await Offer.findOne({
[/* My attributes */],
where: { /* My conditions */ }
});
And a second one, selecting from model Batch, without subQuery: false option, because is not needed anymore.
const batches = await Batch.findAndCountAll({
attributes: [
'id',
'name',
],
where: { offer_id: offer.id },
order: [/* My ordenation */],
include: [
{
model: File,
as: 'gallery',
attributes: ['id', 'path', 'url'],
},
],
limit: 12,
offset: (page - 1) * 12,
});
this below Sequelize work fine for me without using order, i'm wondering why i can't use order for root table as posts model? when i use this below code i get this error:
Unhandled rejection Error: 'posts' in order / group clause is not valid association
but that work fine on other models such as channelVideoContainer
models.posts.findAll({
where: {
channelId: 1
},
include: [
{
model: models.channelVideoContainer,
include: [models.fileServerSetting]
}, {
model: models.channelMusicContainer,
include: [models.fileServerSetting]
}, {
model: models.channelImageWithTextContainer,
include: [models.fileServerSetting]
}, {
model: models.channelFilesContainer,
include: [models.fileServerSetting]
},
models.channelPlainTextContainer
], order: [
[{model: models.posts}, 'id', 'DESC'],
], limit: 5
}).then(function (result) {
console.log(JSON.stringify(result));
});
You are getting this error because you are querying the posts table/model, and then sorting by a column on the posts model, however you are specifying a "joined" table in your order. This works for your other models because they are in fact joined (using the include option). Since you are querying the posts model you just need to pass in the name of the column you want to order by. See some of the ORDER examples in the documentation.
// just specify the 'id' column, 'post' is assumed because it is the queried Model
order: [['id', 'DESC']],
As a side note, you may want to specify required: false on your include'd models to perform a LEFT JOIN so that rows come back even if there are no matches in the joined table. If you know that rows will be returned (or they are actually required) then leave it as is.
{
model: models.channelFilesContainer,
include: [models.fileServerSetting],
required: false, // LEFT JOIN the channelFilesContainer model
},
My database model is as follows:
An employee drives one or zero vehicles
A vehicle can be driven by one or more employees
A vehicle has a model type that tells us it's fuel type amongst other things.
I'd like sequelize to fetch me all employees where they don't drive a vehicle, or if they do then the vehicle is not diesel.
So where VehicleID is null OR Vehicle.VehicleModel.IsDiesel = false
My current code is as follows:
var employee = sequelize.define('employee', {
ID: Sequelize.INTEGER,
VehicleID: Sequelize.INTEGER
});
var vehicle = sequelize.define('vehicle', {
ID: Sequelize.INTEGER,
ModelID: Sequelize.INTEGER
});
var vehicleModel = sequelize.define('vehicleModel', {
ID: Sequelize.INTEGER,
IsDiesel: Sequelize.BOOLEAN
});
employee.belongsTo(vehicle);
vehicle.belongsTo(vehicleModel);
If I run the following:
options.include = [{
model: model.Vehicle,
attributes: ['ID', 'ModelID'],
include: [
{
model: model.VehicleModel,
attributes: ['ID', 'IsDiesel']
}]
}];
employee
.findAll(options)
.success(function(results) {
// do stuff
});
Sequelize does a left outer join to get me the included tables. So I get employees who drive vehicles and who don't.
As soon as I add a where to my options:
options.include = [{
model: model.Vehicle,
attributes: ['ID', 'ModelID'],
include: [
{
model: model.VehicleModel,
attributes: ['ID', 'IsDiesel']
where: {
IsDiesel: false
}
}]
}];
Sequelize now does an inner join to get the included tables.
This means that I only get employees who drive a vehicle and the vehicle is not diesel. The employees who don't drive a vehicle are excluded.
Fundamentally, I need a way of telling Sequelize to do a left outer join and at the same time have a where condition that states the column from the joined table is false or null.
EDIT:
It turns out that the solution was to use required: false, as below:
options.include = [{
model: model.Vehicle,
attributes: ['ID', 'ModelID'],
include: [
{
model: model.VehicleModel,
attributes: ['ID', 'IsDiesel']
where: {
IsDiesel: false
},
required: false
}],
required: false
}];
I had already tried putting the first 'required:false' but I missed out on putting the inner one. I thought it wasn't working so I gave up on that approach. Dajalmar Gutierrez's answer made me realise I needed both for it to work.
When you add a where clause, sequelize automatically adds a required: true clause to your code.
Adding required: false to your include segment should solve the problem
Note: you should check this issue iss4019
Eager loading
When you are retrieving data from the database there is a fair chance that you also want to get associations with the same query - this is called eager loading. The basic idea behind that, is the use of the attribute include when you are calling find or findAll.
when you set
required: false
will do
LEFT OUTER JOIN
when
required: true
will do
INNER JOIN
for more detail docs.sequelizejs eager-loading
I have two tables (users and games) joined by an association table (game_players), creating a many-to-many relationship:
models.Game.belongsToMany(models.User, { through: models.GamePlayer, as: 'players' });
models.User.belongsToMany(models.Game, { through: models.GamePlayer, foreignKey: 'user_id' });
In addition to the foreign keys user_id and game_id, game_players has a few extra columns for link-specific data:
sequelize.define('game_player', {
isReady: {
defaultValue: false,
type: Sequelize.BOOLEAN,
field: 'is_ready'
},
isDisabled: {
defaultValue: false,
type: Sequelize.BOOLEAN,
field: 'is_disabled'
},
powerPreferences: {
type: Sequelize.TEXT,
field: 'power_preferences'
},
power: {
type: Sequelize.STRING(2),
defaultValue: '?'
}
}, {
underscored: true
});
Suppose I want to fetch a game and eagerly load active players. This was my first effort:
db.models.Game.findAll({
include: [{
model: db.models.User,
as: 'players',
where: { 'game_player.isDisabled': false }
}]
}).nodeify(cb);
This generates the following SQL, which throws the error Column players.game_player.isDisabled does not exist:
SELECT "game"."id",
"game"."name",
"game"."description",
"game"."variant",
"game"."status",
"game"."move_clock" AS "moveClock",
"game"."retreat_clock" AS "retreatClock",
"game"."adjust_clock" AS "adjustClock",
"game"."max_players" AS "maxPlayers",
"game"."created_at",
"game"."updated_at",
"game"."gm_id",
"game"."current_phase_id",
"players"."id" AS "players.id",
"players"."email" AS "players.email",
"players"."temp_email" AS "players.tempEmail",
"players"."password" AS "players.password",
"players"."password_salt" AS "players.passwordSalt",
"players"."action_count" AS "players.actionCount",
"players"."failed_action_count" AS "players.failedActionCount",
"players"."created_at" AS "players.created_at",
"players"."updated_at" AS "players.updated_at",
"players.game_player"."is_ready" AS
"players.game_player.isReady",
"players.game_player"."is_disabled" AS
"players.game_player.isDisabled",
"players.game_player"."power_preferences" AS
"players.game_player.powerPreferences",
"players.game_player"."power" AS "players.game_player.power",
"players.game_player"."created_at" AS
"players.game_player.created_at",
"players.game_player"."updated_at" AS
"players.game_player.updated_at",
"players.game_player"."game_id" AS
"players.game_player.game_id",
"players.game_player"."user_id" AS
"players.game_player.user_id"
FROM "games" AS "game"
INNER JOIN ("game_players" AS "players.game_player"
INNER JOIN "users" AS "players"
ON "players"."id" = "players.game_player"."user_id")
ON "game"."id" = "players.game_player"."game_id"
AND "players"."game_player.isdisabled" = false;
Clearly Sequelize is wrapping my constraint alias with incorrect quotes: 'players'.'game_player.isdisabled' should be 'players.game_player'.isdisabled. How can I revise my Sequelize code above to correctly query this column?
I got it, but only through manually browsing the repository's closed tickets and coming upon #4880.
Clauses using joined table columns that don't work out of the box can be wrapped in $. I honestly don't understand its magic, because I swear I don't see any documentation for it. Modifying my query above achieved what I wanted:
db.models.Game.findAll({
include: [{
model: db.models.User,
as: 'players',
where: { '$players.game_player.is_disabled$': false }
}]
}).nodeify(cb);
After searching around, I found that through.where can also be used:
db.models.Game.findAll({
include: [{
model: db.models.User,
as: 'players',
through: { where: { isDisabled: false } }
}]
})
Reference:
Is it possible to filter a query by the attributes in the association table with sequelize?
Eager loading with Many-to-Many relationships
Your query should be on the join table with the 'where' condition, and then you should use the 'include' clause to include the two other models, like this:
db.models.GamePlayer.findAll({
where: {isDisabled: false},
attributes: [],
include: [models.User, models.Game]
}).then(function(result){
....
});