How can I join same table using sequalizer? - node.js

I am using sequalizer first time and stuck in a situation
I have a table categories as below
I need details like
[{
"id" : 7,
"name": "Mobile Cover",
parent: {
"id": 1
"name": "Mobile",
}
},
{
"id" : 9,
"name": "Mobile Glass",
parent: {
"id": 1
"name": "Mobile",
}
},
{
"id" : 8,
"name": "Knife",
parent: {
"id": 4
"name": "Kitchenware",
}
}]
I try with include but it's not working
const result = await category.findAll({
where: {
parentId: {
[Op.ne]: null
}
},
include: {
model: category,
where: {
'id': 'category.parentId'
}
},
order: [
["createdAt", "DESC"],
],
});
it's returning error
SequelizeDatabaseError: table name "categories" specified more than
once
Is there any way to do this?

Sequelize version: "sequelize": "^5.21.3". Here is a working example:
index.ts:
import { sequelize } from '../../db';
import Sequelize, { Model, DataTypes, Op } from 'sequelize';
class Category extends Model {}
Category.init(
{
id: {
primaryKey: true,
autoIncrement: true,
allowNull: false,
type: DataTypes.INTEGER,
},
name: DataTypes.STRING,
},
{ sequelize, modelName: 'categories' },
);
Category.belongsTo(Category, { foreignKey: 'parentId', as: 'parent', targetKey: 'id' });
(async function test() {
try {
await sequelize.sync({ force: true });
// seed
const parent1 = { id: 1, name: 'Mobile' };
const parent2 = { id: 4, name: 'Kitchenware' };
await Category.bulkCreate([parent1, parent2]);
await Category.bulkCreate([
{ id: 7, name: 'Mobile Cover', parentId: parent1.id },
{ id: 8, name: 'Knife', parentId: parent2.id },
{ id: 9, name: 'Mobile Glass', parentId: parent1.id },
]);
// test
const result = await Category.findAll({
include: [
{
model: Category,
required: true,
as: 'parent',
attributes: ['id', 'name'],
},
],
attributes: ['id', 'name'],
raw: true,
});
console.log('result: ', result);
} catch (error) {
console.log(error);
} finally {
await sequelize.close();
}
})();
The execution results:
Executing (default): DROP TABLE IF EXISTS "categories" CASCADE;
Executing (default): DROP TABLE IF EXISTS "categories" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "categories" ("id" SERIAL , "name" VARCHAR(255), "parentId" INTEGER REFERENCES "categories" ("id") ON DELETE SET NULL ON UPDATE CASCADE, PRIMARY KEY ("id"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'categories' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): INSERT INTO "categories" ("id","name") VALUES (1,'Mobile'),(4,'Kitchenware') RETURNING *;
Executing (default): INSERT INTO "categories" ("id","name","parentId") VALUES (7,'Mobile Cover',1),(8,'Knife',4),(9,'Mobile Glass',1) RETURNING *;
Executing (default): SELECT "categories"."id", "categories"."name", "parent"."id" AS "parent.id", "parent"."name" AS "parent.name" FROM "categories" AS "categories" INNER JOIN "categories" AS "parent" ON "categories"."parentId" = "parent"."id";
result: [ { id: 7,
name: 'Mobile Cover',
'parent.id': 1,
'parent.name': 'Mobile' },
{ id: 8,
name: 'Knife',
'parent.id': 4,
'parent.name': 'Kitchenware' },
{ id: 9,
name: 'Mobile Glass',
'parent.id': 1,
'parent.name': 'Mobile' } ]
Check the data records in the database:
node-sequelize-examples=# select * from "categories";
id | name | parentId
----+--------------+----------
1 | Mobile |
4 | Kitchenware |
7 | Mobile Cover | 1
8 | Knife | 4
9 | Mobile Glass | 1
(5 rows)

You need to define an association as follows -
Category.belongsTo(Category, {
foreignKey: "parentID",
targetKey: "id"
});

Before using include you need to define associations on the basis of their relations. Sequelize has all traditional types of relationships like hasMany, BelogTo, etc.
furthermore check out the link: https://sequelize.org/master/manual/associations.html

Related

Get number of associated elements in Sequelize

So I have a many-to-many relationship with Sequelize. This code gives me an array of all the categories associated with the Post. It works to get this data. However, if I would like to make that list of categories into just a single key value pair of how many categories instead of the categories. How could I do that?
return models.Post.findAndCountAll({
limit: limit,
offset: offset,
include: [{
model: models.Category,
as: 'categories',
required: false,
}],
})
For example this is the current output:
{
"id": 1,
"name": "foo",
"categories": [
{
"id": 1,
"name": "bar"
}
]
}
The desired output:
{
"id": 1,
"name": "foo",
"categories": 10
}
EDIT: As suggestions for fixing this I tried doing this:
return models.Post.findAndCountAll({
group: ['post.id'],
attributes: {
include: [[db.sequelize.fn("COUNT", db.sequelize.col("categories.id")), "categoriesCount"]]
},
limit: limit,
offset: offset,
include: [{
model: models.Category,
as: 'categories',
required: true,
attributes: []
}],
raw: true,
subQuery: false
})
But that just gives me the error:
{
"message": "invalid reference to FROM-clause entry for table \"post\""
}
This is basically what I want to get back, i wrote it in SQL and tried it:
SELECT
cp.category_id as category_id,
p.name as post_name,
COUNT(p.id) as num_categories
FROM
category c,
category_post cp
JOIN
post p ON p.id = cp.category_id
WHERE
p.id = cp.post_id AND
p.created_at >= '2022-01-26' and p.created_at <= '2022-05-02'
GROUP BY
cp.category_id,
post_name
ORDER BY
num_categories DESC
Generated SQL with Sequelize:
Executing (default): SELECT "post"."id", count("post"."id") AS "count" FROM "post" AS "post" INNER JOIN ( "category_post" AS "categories->categoryPost" INNER JOIN "category" AS "categories" ON "categories"."id" = "categories->categoryPost"."category_id") ON "post"."id" = "categories->categoryPost"."post_id" GROUP BY "post"."id";
Executing (default): SELECT "post"."id", "post"."name", COUNT("categories"."id") AS "categoryCount", "categories->categoryPost"."id" AS "categories.categoryPost.id", "categories->categoryPost"."category_id" AS "categories.categoryPost.category_id", "categories->categoryPost"."post_id" AS "categories.categoryPost.post_id" FROM "post" AS "post" INNER JOIN ( "category_post" AS "categories->categoryPost" INNER JOIN "category" AS "categories" ON "categories"."id" = "categories->categoryPost"."category_id") ON "post"."id" = "categories->categoryPost"."post_id" GROUP BY "post"."id" LIMIT 3 OFFSET 0;
My models look like the following:
Post(id, name, created_at, updated_at)
Category(id, name,)
PostCategory(id, post_id,category_id)
In my Post model:
static associate(models) {
this.belongsToMany(models.Category, {
through: models.CategoryPost,
as: 'posts',
foreignKey: 'category_id',
onDelete: 'CASCADE'
})
}
In my Category model:
static associate(models) {
this.belongsToMany(models.Post, {
through: models.CategoryPost,
as: 'categories',
foreignKey: 'post_id',
onDelete: 'CASCADE'
})
}
The generated SQL based on Emma's answer:
Executing (default): SELECT count("Post"."id") AS "count" FROM "Post" AS "Post" INNER JOIN ( "category_Post" AS "categories->categoryPost" INNER JOIN "category" AS "categories" ON "categories"."id" = "categories->categoryPost"."category_id") ON "Post"."id" = "categories->categoryPost"."Post_id";
Executing (default): SELECT "Post"."id", "Post"."name", (COUNT("categories"."id") OVER (PARTITION BY "Post"."id")::int) AS "categories" FROM "Post" AS "Post" INNER JOIN ( "category_Post" AS "categories->categoryPost" INNER JOIN "category" AS "categories" ON "categories"."id" = "categories->categoryPost"."category_id") ON "Post"."id" = "categories->categoryPost"."Post_id" LIMIT 3 OFFSET 0;
If Post is the parent and Category is the child and you want to find the number of categories for a given post... you can u se the following way..
return models.Post.findAll({
attributes: {
include: [[Sequelize.fn("COUNT", Sequelize.col("categories.id")), "cetegoryCount"]]
},
include: [{
model: models.Category,
as: 'categories'
}],
})
group in Postgres usually have some issues(Aggregate funcction issues).
Alternatively, you can use OVER PARTITION BY syntax which usually works in this situation.
const posts = await models.Post.findAndCountAll({
attributes: {
include: [[db.sequelize.literal('(COUNT("categories"."id") OVER (PARTITION BY "post"."id")::int)'), 'categories']]
},
limit: limit,
offset: offset,
include: [{
model: models.Category,
as: 'categories',
required: true,
attributes: [],
through: {
attributes: []
}
}],
raw: true,
subQuery: false
})
This should return something like this.
{
"result": {
"count": 2, // This count is for post
"rows": [
{
"id": 1,
"name": "post",
"categories": 2,
...
}
]
}
}

left outer join with custom ON condition sequelize

I'm using sequelize, DB as Postgres
my students table as ( id, name, dept_id ) columns.
my departments table as ( id, dname, hod) columns
My Association while looks like this
Student.hasOne(models.Department, {
foreignKey: 'id',
as : "role"
});
I want to get a student with a particular id and his department details. By using sequelize I wrote a query like below
Student.findOne({
where: { id: id },
include: [{
model: Department,
as:"dept"
}],
})
FYI with this includes option in findOne, it is generated a left outer join query as below
SELECT "Student"."id", "Student"."name", "Student"."dept_id", "dept"."id" AS "dept.id", "dept"."dname" AS "dept.dname", "dept"."hod" AS "dept.hod", FROM "students" AS "Student"
LEFT OUTER JOIN "departments" AS "dept"
ON "Student"."id" = "dept"."id"
WHERE "Student"."id" = 1
LIMIT 1;
My expected Output should be
{
id: 1,
name: "bod",
dept_id: 4,
dept: {
id: 4,
dname: "xyz",
hod: "x"
}
}
but I'm getting
{
id: 1,
name: "bod",
dept_id: 4,
dept: {
id: 1,
dname: "abc",
hod: "a"
}
}
so how to I change ON condition to
ON "Student"."dept_id" = "dept"."id"
thank you in advance
Student.hasOne(models.Department, {
foreignKey: 'dept_id', // fixed
as : "role"
});

sequelize model exclude subfield within a jsonb field

I have a postgresql table structured with a jsonb field named external_data:
external_data: {a: 1, b:2, c:3}
I know that in sequelize model definitions, you can exclude a field by including in the options properties a defaultScope like this:
{defaultScope: {attributes: { exclude: ['external_data'] }}
When I do that to exclude the entire field, it works, i.e. external_data doesn't show up in returned results. However, I'd like to exclude a specific subfield. I've tried exclude: ['external_data.b'] but that doesn't seem to work.
Does sequelize allow excluding subfields within jsonb fields? And if so, what's the proper format of the exclude attribute to do it?
From the docs: https://sequelize.org/v5/manual/scopes.html
Scopes are defined in the model definition and can be finder objects, or functions returning finder objects - except for the default scope, which can only be an object
So, you can define the scope via a function that returns the finder objects so that you can implement the complex scope.
E.g.
import { sequelize } from '../../db';
import { Model, DataTypes, Op } from 'sequelize';
class TableA extends Model {}
TableA.init(
{
external_data: DataTypes.JSONB,
},
{
sequelize,
modelName: 'table_a',
defaultScope: {
attributes: { exclude: ['external_data'] },
},
scopes: {
exclude(subField) {
return {
where: {
external_data: {
[subField]: {
[Op.eq]: null,
},
},
},
};
},
},
},
);
(async function test() {
try {
// create tables
await sequelize.sync({ force: true });
// seed
await TableA.bulkCreate([
{ external_data: { a: 1, b: 2, c: 3 } },
{ external_data: { a: 10, c: 30 } },
{ external_data: { a: 100 } },
]);
// test
// test case 1: exclude subfield "b"
const result1 = await TableA.scope({ method: ['exclude', 'b'] }).findAll({ raw: true });
console.log('result1:', result1);
// test case 2: exclude subfield "c"
const result2 = await TableA.scope({ method: ['exclude', 'c'] }).findAll({ raw: true });
console.log('result2:', result2);
} catch (error) {
console.log(error);
} finally {
await sequelize.close();
}
})();
The execution results:
Executing (default): DROP TABLE IF EXISTS "table_a" CASCADE;
Executing (default): DROP TABLE IF EXISTS "table_a" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "table_a" ("id" SERIAL , "external_data" JSONB, PRIMARY KEY ("id"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'table_a' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): INSERT INTO "table_a" ("id","external_data") VALUES (DEFAULT,'{"a":1,"b":2,"c":3}'),(DEFAULT,'{"a":10,"c":30}'),(DEFAULT,'{"a":100}') RETURNING *;
Executing (default): SELECT "id", "external_data" FROM "table_a" AS "table_a" WHERE ("table_a"."external_data"#>>'{b}') IS NULL;
result1: [ { id: 2, external_data: { a: 10, c: 30 } },
{ id: 3, external_data: { a: 100 } } ]
Executing (default): SELECT "id", "external_data" FROM "table_a" AS "table_a" WHERE ("table_a"."external_data"#>>'{c}') IS NULL;
result2: [ { id: 3, external_data: { a: 100 } } ]
check the database:
node-sequelize-examples=# select * from "table_a";
id | external_data
----+--------------------------
1 | {"a": 1, "b": 2, "c": 3}
2 | {"a": 10, "c": 30}
3 | {"a": 100}
(3 rows)
Dependencies versions: "sequelize": "^5.21.3", postgres:9.6
source code: https://github.com/mrdulin/node-sequelize-examples/tree/master/src/examples/stackoverflow/61053931

sequelize methods from association fooInstance.getBars() returning a wrong value

I've got 2 models with a many-to-many association through another model. By default, Sequelize creates instance methods for both models as you all may know (https://sequelize.org/master/manual/assocs.html#special-methods-mixins-added-to-instances).
My problem is that the fooInstance.getBars() method and fooInstance.countBars() both return the wrong number of records.
The database was setup using sequelize migration files.
The 3 models are below.
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
firstName: DataTypes.STRING
},
{}
);
User.associate = function(models) {
// associations can be defined here
User.belongsToMany(models.Week, {
through: models.Roster,
foreignKey: {
name: "weekId"
}
});
};
return User;
};
"use strict";
module.exports = (sequelize, DataTypes) => {
const Week = sequelize.define(
"Week",
{
date: {
type: DataTypes.DATE,
allowNull: false,
unique: true
}
},
{}
);
Week.associate = function(models) {
// associations can be defined here
Week.belongsToMany(models.User, {
through: models.Roster,
foreignKey: {
name: "userId",
allowNull: true
}
});
};
return Week;
};
"use strict";
module.exports = (sequelize, DataTypes) => {
const Roster = sequelize.define(
"Roster",
{
weekId: {
type: DataTypes.INTEGER,
allowNull: false,
unique: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: true,
unique: false
}
},
{}
);
Roster.associate = function(models) {
// associations can be defined here
};
return Roster;
};
Before I test out the methods, I created 1 record in weeks and 5 records in users then I created 5 associations using 5 Roster.create() in rosters.
When I do a query in my routes file like this await Week.getUsers(), instead of returning 5 users in an array, it returned 1 in an array. Similarly, the await Week.countUsers() code returned 1 instead of 5.
Please help!
Please let me know if I miss any important information.
Thank you!
Like the docs says:
When an association is defined between two models, the instances of those models gain special methods to interact with their associated counterparts.
You are trying to call these special methods on Model class. Here is a working example:
import { sequelize } from '../../db';
import { Model, DataTypes, BelongsToManyGetAssociationsMixin, BelongsToManyCountAssociationsMixin } from 'sequelize';
class User extends Model {}
User.init(
{
firstName: DataTypes.STRING,
},
{ sequelize, modelName: 'User' },
);
class Week extends Model {
public getUsers!: BelongsToManyGetAssociationsMixin<User>;
public countUsers!: BelongsToManyCountAssociationsMixin;
}
Week.init(
{
date: {
type: DataTypes.DATE,
allowNull: false,
unique: true,
},
},
{ sequelize, modelName: 'Week' },
);
class Roster extends Model {}
Roster.init(
{
weekId: {
type: DataTypes.INTEGER,
allowNull: false,
unique: false,
},
userId: {
type: DataTypes.INTEGER,
allowNull: true,
unique: false,
},
},
{ sequelize, modelName: 'Roster' },
);
User.belongsToMany(Week, { through: Roster, foreignKey: { name: 'weekId' } });
Week.belongsToMany(User, { through: Roster, foreignKey: { name: 'userId', allowNull: true } });
(async function test() {
try {
// create tables
await sequelize.sync({ force: true });
// seed
const week = await Week.create(
{
date: new Date(),
Users: [
{ firstName: 'james' },
{ firstName: 'elsa' },
{ firstName: 'tim' },
{ firstName: 'lee' },
{ firstName: 'jasmine' },
],
},
{ include: [User] },
);
// test
const count = await week.countUsers();
console.log('count:', count);
const users = await week.getUsers();
console.log('users count:', users.length);
} catch (error) {
console.log(error);
} finally {
await sequelize.close();
}
})();
The execution results:
Executing (default): DROP TABLE IF EXISTS "Roster" CASCADE;
Executing (default): DROP TABLE IF EXISTS "Week" CASCADE;
Executing (default): DROP TABLE IF EXISTS "User" CASCADE;
Executing (default): DROP TABLE IF EXISTS "User" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "User" ("id" SERIAL , "firstName" VARCHAR(255), PRIMARY KEY ("id"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'User' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): DROP TABLE IF EXISTS "Week" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "Week" ("id" SERIAL , "date" TIMESTAMP WITH TIME ZONE NOT NULL UNIQUE, PRIMARY KEY ("id"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'Week' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): DROP TABLE IF EXISTS "Roster" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "Roster" ("weekId" INTEGER NOT NULL REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE, "userId" INTEGER REFERENCES "Week" ("id") ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE ("weekId", "userId"), PRIMARY KEY ("weekId","userId"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'Roster' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): INSERT INTO "Week" ("id","date") VALUES (DEFAULT,$1) RETURNING *;
Executing (default): INSERT INTO "User" ("id","firstName") VALUES (DEFAULT,$1) RETURNING *;
Executing (default): INSERT INTO "User" ("id","firstName") VALUES (DEFAULT,$1) RETURNING *;
Executing (default): INSERT INTO "User" ("id","firstName") VALUES (DEFAULT,$1) RETURNING *;
Executing (default): INSERT INTO "User" ("id","firstName") VALUES (DEFAULT,$1) RETURNING *;
Executing (default): INSERT INTO "User" ("id","firstName") VALUES (DEFAULT,$1) RETURNING *;
Executing (default): INSERT INTO "Roster" ("weekId","userId") VALUES ($1,$2) RETURNING *;
Executing (default): INSERT INTO "Roster" ("weekId","userId") VALUES ($1,$2) RETURNING *;
Executing (default): INSERT INTO "Roster" ("weekId","userId") VALUES ($1,$2) RETURNING *;
Executing (default): INSERT INTO "Roster" ("weekId","userId") VALUES ($1,$2) RETURNING *;
Executing (default): INSERT INTO "Roster" ("weekId","userId") VALUES ($1,$2) RETURNING *;
Executing (default): SELECT COUNT("User"."id") AS "count" FROM "User" AS "User" INNER JOIN "Roster" AS "Roster" ON "User"."id" = "Roster"."weekId" AND "Roster"."userId" = 1;
count: 5
Executing (default): SELECT "User"."id", "User"."firstName", "Roster"."weekId" AS "Roster.weekId", "Roster"."userId" AS "Roster.userId" FROM "User" AS "User" INNER JOIN "Roster" AS "Roster" ON "User"."id" = "Roster"."weekId" AND "Roster"."userId" = 1;
users count: 5
check the database:
node-sequelize-examples=# select * from "User";
id | firstName
----+-----------
1 | james
2 | elsa
3 | tim
4 | lee
5 | jasmine
(5 rows)
node-sequelize-examples=# select * from "Week";
id | date
----+---------------------------
1 | 2020-04-14 01:56:56.55+00
(1 row)
node-sequelize-examples=# select * from "Roster";
weekId | userId
--------+--------
1 | 1
2 | 1
3 | 1
4 | 1
5 | 1
(5 rows)
Dependencies versions: "sequelize": "^5.21.3", postgres:9.6
source code: https://github.com/mrdulin/node-sequelize-examples/tree/master/src/examples/stackoverflow/60805725

Sequelize include same association twice with different criteria

Say I have a table suppliers, which is associated to tags through another table suppliers_tags. So my model looks like this:
const Supplier = Sequelize.define('suppliers', {
id: { type: Sequelize.INTEGER, primaryKey: true },
name: Sequelize.STRING,
});
const SupplierTag = Sequelize.define('suppliers_tags', {
id: { type: Sequelize.INTEGER, primaryKey: true },
supplier_id: { type: Sequelize.INTEGER, references: { model: 'suppliers', key: 'id' } },
tag_id: { type: Sequelize.INTEGER, references: { model: 'suppliers', key: 'id' } },
});
const Tag = Sequelize.define('tags', {
id: { type: Sequelize.INTEGER, primaryKey: true },
name: Sequelize.STRING,
type: Sequelize.ENUM('food', 'drink'),
});
The associations look like this:
Supplier.belongsToMany(Tag, { as: 'tags', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Tag.belongsToMany(Supplier, { as: 'supplierTag', through: 'suppliers_tags', foreignKey: 'tag_id' });
Let's say I have the following data in the database:
suppliers:
id name
1 Supplier1
2 Supplier2
3 Supplier3
tags:
id name type
1 Food1 food
2 Vegan food
3 Vegan drink
4 Food2 food
(I purposely named two tags the same, as for this application, it is important that tags with different types can have the same name.)
suppliers_tags
id supplier_id tag_id
1 1 1
2 1 3
3 1 2
4 2 1
5 2 4
6 3 2
Now I can do the following query:
Supplier.findAll({
include: [
{
model: Tag,
as: 'tags',
where: {
[Op.and]: [
{ type: 'food' },
{ name: 'Vegan' },
],
},
},
],
});
This returns Supplier1 and Supplier3, correctly joining on suppliers_tags and tags and filtering the tags table to include ones of type 'food' and name 'Vegan'.
Now, what if I want to search for suppliers where both the following conditions hold:
The supplier has an associated tag which is of type food and name Food1
The supplier has an associated tag which is of type drink and name Vegan
Naively (?), I tried the following:
Supplier.findAll({
include: [
{
model: Tag,
as: 'tags_food',
where: {
[Op.and]: [
{ type: 'food' },
{ name: 'Food1' },
],
},
},
{
model: Tag,
as: 'tags_drink',
where: {
[Op.and]: [
{ type: 'drink' },
{ name: 'Vegan' },
],
},
},
],
});
This tries to join the tags table twice, but neglecting to add aliases, resulting in the following error:
SequelizeEagerLoadingError: tags is associated to suppliers multiple times. To identify the correct association, you must use the 'as' keyword to specify the alias of the association you want to include.
Right, so it seems like the as options on the include didn't have the desired effect. What if I modify the associations to be the following:
Supplier.belongsToMany(Tag, { as: 'tags_drink', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Supplier.belongsToMany(Tag, { as: 'tags_food', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Tag.belongsToMany(Supplier, { as: 'supplierTag', through: 'suppliers_tags', foreignKey: 'tag_id' });
Now, if I run the same findAll query, it generates the following SQL:
SELECT
"suppliers".*
,"tags_food"."id" AS "tags_food.id"
,"tags_food"."name" AS "tags_food.name"
,"tags_food"."type" AS "tags_food.type"
,"tags_food->suppliers_tags"."supplier_id" AS "tags_food.suppliers_tags.supplier_id"
,"tags_food->suppliers_tags"."tag_id" AS "tags_food.suppliers_tags.tag_id"
,"tags_drink"."id" AS "tags_drink.id"
,"tags_drink"."name" AS "tags_drink.name"
,"tags_drink"."type" AS "tags_drink.type"
,"tags_drink->suppliers_tags"."supplier_id" AS "tags_drink.suppliers_tags.supplier_id"
,"tags_drink->suppliers_tags"."tag_id" AS "tags_drink.suppliers_tags.tag_id"
FROM (
SELECT
"suppliers"."id"
,"suppliers"."name"
FROM "suppliers" AS "suppliers"
WHERE (
SELECT "suppliers_tags"."supplier_id"
FROM "suppliers_tags" AS "suppliers_tags"
INNER JOIN "tags" AS "tag" ON "suppliers_tags"."tagId" = "tag"."id"
AND ("tag"."type" = 'food' AND "tag"."name" = 'Food1')
WHERE ("suppliers"."id" = "suppliers_tags"."supplier_id")
LIMIT 1
) IS NOT NULL
AND (
SELECT "suppliers_tags"."supplier_id"
FROM "suppliers_tags" AS "suppliers_tags"
INNER JOIN "tags" AS "tag" ON "suppliers_tags"."tag_id" = "tag"."id"
AND ("tag"."type" = 'drink' AND "tag"."name" = 'Vegan')
WHERE ("suppliers"."id" = "suppliers_tags"."supplier_id")
LIMIT 1
) IS NOT NULL
) AS "suppliers"
INNER JOIN (
"suppliers_tags" AS "tags_food->suppliers_tags"
INNER JOIN "tags" AS "tags_food"
ON "tags_food"."id" = "tags_food->suppliers_tags"."tagId"
) ON "suppliers"."id" = "tags_food->suppliers_tags"."supplier_id"
AND ("tags_food"."type" = 'food' AND "tags_food"."name" = 'Food1')
INNER JOIN (
"suppliers_tags" AS "tags_drink->suppliers_tags"
INNER JOIN "tags" AS "tags_drink"
ON "tags_drink"."id" = "tags_drink->suppliers_tags"."tag_id"
) ON "suppliers"."id" = "tags_drink->suppliers_tags"."supplier_id"
AND ("tags_drink"."type" = 'drink' AND "tags_drink"."name" = 'Vegan')
ORDER BY "suppliers"."id"
This is correct and what I need, except for one error: the foreign key tag_id has been switched to tagId (?!) in the case of the tags_food join. This then of course fails with the following error:
SequelizeDatabaseError: column tags_food->suppliers_tags.tagId does not exist
Notice that the correct foreign key tag_id was generated in the join on tags_drink by Sequelize.
If I switch around the relation definitions, i.e.:
Supplier.belongsToMany(Tag, { as: 'tags_food', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Supplier.belongsToMany(Tag, { as: 'tags_drink', through: 'suppliers_tags', foreignKey: 'supplier_id' });
Tag.belongsToMany(Supplier, { as: 'supplierTag', through: 'suppliers_tags', foreignKey: 'tag_id' });
Then tag_id is produced successfully for the tags_food join and the weird tagId is produced for the tags_drink join.
Can I put this down to another basic flaw with Sequelize, or:
Is there a defined way of joining the same relation twice on different criteria in Sequelize?

Resources