left outer join with custom ON condition sequelize - node.js

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"
});

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,
...
}
]
}
}

How to get top two salaried employees in mongodb

I want to get top two employees based on salary from a department,
Department Schema ::
const departmentSchema = mongoose.Schema({
name: {
type: String
},
employee_id: [{ type: mongoose.Schema.Types.ObjectId, ref: 'employee' }]
})
Employee Schema ::
const employeeSchema = mongoose.Schema({
name: {
type: String,
},
salary : {
type: Number
}
})
Let's say I have document in department schema as ::
{
"_id":ObjectId("615851c162a012f82cc5bdf6"),
"name":"abc",
"employee_id":[
ObjectId("615852d062a012f82cc5d54d"),
ObjectId("6158530462a012f82cc5d9c8"),
ObjectID("6158530462a012f82cc8e1b9")
]
}
And document in employee as ::
{
"_id":ObjectId("615852d062a012f82cc5d54d"),
"name":"john",
"salary":12345
}
{
"_id":ObjectId("6158530462a012f82cc5d9c8"),
"name":"smith",
"salary":999
}
{
"_id":ObjectId("6158530462a012f82cc8e1b9"),
"name":"Alex",
"salary":99999
}
Based on this I want to get details of top two employees based on their salary.
For this I tried as:
await department.aggregate([
{ $match : { name : 'abc' } },
]).populated('employee_id')
But it throws error populate is not a function. How can I work for the same as I am very much in MySQL but not familiar with mongodb? If anyone needs any further information please do let me know.
Expected response as ::
[
{
"_id":ObjectId("6158530462a012f82cc8e1b9"),
"name":"Alex",
"salary":99999,
department: abc
},
{
"_id":ObjectId("615852d062a012f82cc5d54d"),
"name":"john",
"salary":12345,
department: abc
}
]
You can do the followings in an aggregation pipeline:
$match to select the expected department document
$lookup from employee collection with a subpipeline
In the subpipeline of step 2, use $match to filter out expected employee records
In the same subpipeline , $sort by salary and $limit to get top 2 employee
use $addFields to add the department name to the employee object
$unwind the subpipeline result
$replaceRoot to get the employee documents
Here is the Mongo playground for your reference.

sequelize get column value of referenced table

I have three tables meetings, rooms and projects
meetings.belongsTo(models.rooms, { foreignKey: "room_id" , targetKey: "id"});
rooms.hasMany(models.meetings, { foreignKey : "id", targetKey : "room_id"});
rooms.belongsTo(models.projects, { foreignKey: "project_id", targetKey: "id"});
projects.hasMany(models.rooms, {foreignKey:"id", targetKey:"project_id"});
id, room_id and project_id are primary key of their respective table.
I want to find value of some column of projects for a particular meeting id. How to write a single query using sequelize nodejs to do this?
below query need to execute using sequelize which is giving correct result
select project_meta from projects p
inner join rooms r on p.id = r.project_id
inner join meetings m on r.id = m.room_id
where m.id = "QBZJ0TK7V6NFSPPWGFNCN";
wrote following
projects.findAll({
include: [{
model: rooms,
include: [{
model: meetings,
where: {
id: "QBZJ0TK7V6NFSPPWGFNCN"
}
}]
}],
attributes: ['project_meta']
}
but it is executing different query and giving unexpected result
is there any problem with association?
You must use SQL joins (sequelize's include)
function myQuery(meetingId) {
let options = {};
options.attributes = ['column1', 'column2', ...]; // The specific arrtibutes you need.
options.include = [
{
model: RoomModel,
include: [
{
model: MeetingModel,
where: {
id: meetingId // The specific meeting id
}
}
]
}
];
ProjectModel.findAll(options);
}

How can I join same table using sequalizer?

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

How can I query an associative table with matching "where" conditions with Sequelize

I'm trying to retrieve products that belong to multiple collections using sequelize.
Dialect is Postgres and I'm using Sequelize 4.15.1
I have a Product model, a Collection model and the associative model CollectionProduct. Product belongs to many collections and collection belongs to many products.
Product model:
const Product = sequelize.define( 'Product', {
id: {
type: DataTypes.STRING,
primaryKey: true
},
name: DataTypes.STRING,
} );
Product.associate = function( models ) {
Product.belongsToMany( models.Collection, {
through: models.CollectionProduct
} );
}
Collection model:
const Collection = sequelize.define( 'Collection', {
name: {
type: Sequelize.STRING
},
slug: {
type: Sequelize.STRING
},
} );
Collection.associate = function( models ) {
Collection.belongsToMany( models.Product, {
through: models.CollectionProduct
} );
}
Here is my sequelize query
models.Product.findAll( {
attributes: ["id", "name"],
include: [ {
model: models.Collection,
attributes: ["id", "name", "slug"],
where: {
slug: {
[Op.like]: { [Op.all]: ['collection-a', 'collection-b']}
}
}
} ]
} ).then( products => {
console.log( products )
} ).catch( function ( err ) {
console.log( err );
} );
Here is the generated postgres query
SELECT "Product"."id",
"Product"."name",
"Collections"."id" AS "Collections.id",
"Collections"."name" AS "Collections.name",
"Collections"."slug" AS "Collections.slug",
"Collections->CollectionProduct"."createdAt" AS "Collections.CollectionProduct.createdAt",
"Collections->CollectionProduct"."updatedAt" AS "Collections.CollectionProduct.updatedAt",
"Collections->CollectionProduct"."CollectionId" AS "Collections.CollectionProduct.CollectionId",
"Collections->CollectionProduct"."ProductId" AS "Collections.CollectionProduct.ProductId"
FROM "Products" AS "Product"
INNER JOIN ("CollectionProducts" AS "Collections->CollectionProduct"
INNER JOIN "Collections" AS "Collections" ON "Collections"."id" = "Collections->CollectionProduct"."CollectionId") ON "Product"."id" = "Collections->CollectionProduct"."ProductId"
AND "Collections"."slug" LIKE ALL (ARRAY['collection-a',
'collection-b']);
I'd like to return the products that belong to both collection A and B.
Unfortunately the query execution does not return any result.
This might do the trick. Grabs all productid's intersecting between collection-a id's and collection-b id's.
select
t1.slug as slug_ca,
t2.slug as slug_cb,
t1.productid
from
(
select
p3.slug,
p1.id as productid
from products p1
inner join collectionproducts p2
on p1.id = p2.ProductId
inner join collections p3
on p2.CollectionId = p3.id
where lower(p3.slug) like '%collection-a%'
group by 1,2
) t1
inner join
(
select
p3.slug,
p1.id as productid
from products p1
inner join collectionproducts p2
on p1.id = p2.ProductId
inner join collections p3
on p2.CollectionId = p3.id
where lower(p3.slug) like '%collection-b%'
group by 1,2
) t2
on t1.productid = t2.productid
group by 1,2,3;

Resources