Filter by belongsToMany relation field - sql

It is impossible to filter data using a linked table. There are two tables Instructor and Club. They related how belongsToMany. I need to get all Instructors which club_id = value.
Instructor model:
sequelize.define('Instructor', {
instance_id: DataTypes.INTEGER,
name: DataTypes.STRING(255)
}, {
tableName: 'instructors',
timestamps: false,
classMethods: {
associate: function (models) {
Instructor.belongsToMany(models.Club, {
through: 'InstructorClub'
});
}
}
});
Club model:
sequelize.define('Club', {
instance_id: DataTypes.INTEGER,
name: DataTypes.STRING
}, {
tableName: 'clubs',
timestamps: false,
classMethods: {
associate: function (models) {
Club.belongsToMany(models.Instructor, {
through: 'InstructorClub'
});
}
}
});
Related table:
sequelize.define('InstructorClub', {
InstructorId: {
type: DataTypes.INTEGER,
field: 'instructor_id'
},
ClubId: {
type: DataTypes.INTEGER,
field: 'club_id'
}
}, {
tableName: 'instructors_clubs'
timestamps: false
});
I am trying to get the data as follows::
models
.Instructor
.findAll({
include: [
{
model: models.Club,
as: 'Clubs',
through: {
attributes: []
}
}
],
# I need to filter by club.id
where: {
'Clubs.id': 10
}
})
Current query generated SQL:
SELECT `Instructor`.`id`,
`Instructor`.`instance_id`,
`Instructor`.`name`,
`Clubs`.`id` AS `Clubs.id`,
`Clubs`.`name` AS `Clubs.name`,
`Clubs.InstructorClub`.`club_id` AS `Clubs.InstructorClub.ClubId`,
`Clubs.InstructorClub`.`instructor_id` AS `Clubs.InstructorClub.InstructorId`
FROM `instructors` AS `Instructor`
LEFT OUTER JOIN (`instructors_clubs` AS `Clubs.InstructorClub` INNER JOIN `clubs` AS `Clubs` ON `Clubs`.`id` = `Clubs.InstructorClub`.`club_id`)
ON `Instructor`.`id` = `Clubs.InstructorClub`.`instructor_id`
WHERE `Instructor`.`Clubs.id` = 10;
Well, I need some kind of this:
SELECT `Instructor`.`id`,
`Instructor`.`instance_id`,
`Instructor`.`name`,
`Clubs`.`id` AS `Clubs.id`,
`Clubs`.`name` AS `Clubs.name`,
`Clubs.InstructorClub`.`club_id` AS `Clubs.InstructorClub.ClubId`,
`Clubs.InstructorClub`.`instructor_id` AS `Clubs.InstructorClub.InstructorId`
FROM `instructors` AS `Instructor`
LEFT OUTER JOIN (`instructors_clubs` AS `Clubs.InstructorClub` INNER JOIN `clubs` AS `Clubs` ON `Clubs`.`id` = `Clubs.InstructorClub`.`club_id`)
ON `Instructor`.`id` = `Clubs.InstructorClub`.`instructor_id`
# It should be like this:
WHERE `Clubs`.`id` = 10;

Move your 'where' up into the include (with model, as, and through).
include: [ {
model: models.Club,
as: 'Clubs',
through: { attributes: [] },
where: { 'Clubs.id': 10 }
} ]

Related

Unknown column in sequelize count

I do such node.js Sequelize query to get rows quantity of included unread_messages, so I can get amount of unread messages of specifi user. But it returns me Unknown column 'unread_messages.id' in 'field list'.
If I remove attributes: {...} error disappears
const result = await Chats.findAndCountAll({
attributes: {
include: [[Sequelize.fn('COUNT', Sequelize.col('unread_messages.id')), 'total_unread_messages']]
},
where: {
...(req.query.filters as WhereOptions),
},
include: [
{ model: Users, as: 'createdBy', required: false },
{ model: ChatTypes, as: 'type', required: false },
{
model: ChatMessages,
as: 'unread_messages',
where: {
id: {[Op.gt]: Sequelize.literal(`(
SELECT last_read_message_id
FROM chats_users
WHERE
user_id = '${req.user?.id}'
AND
chat_id = Chats.id
)`),}
},
required: false,
},
{
model: ChatMessages,
as: 'last_message',
required: false,
include: [
{ model: Users, as: 'to_user' },
{ model: Users, as: 'from_user' },
{ model: Chats, as: 'chat' },
{ model: MessageTypes, as: 'message_type' },
{
model: Users,
as: 'is_mine',
required: false,
where: { id: req.user?.id },
},
],
},
],
group:['chats.id'],
order: req.query.sort as Order,
offset,
limit,
});

Sequelize doesn't use field alias from model

I have model called proxyPool with next fields:
poolId: {
type: DataTypes.INTEGER,
references: {
model: 'pool',
key: 'id',
},
field: 'pool_id',
},
proxyId: {
type: DataTypes.INTEGER,
references: {
model: 'proxy',
key: 'id',
},
field: 'proxy_id',
},
It's a N:M table for two tables which have next associations:
proxy.associate = (models) => {
proxy.belongsToMany(models.pool, {
through: models.proxyPool,
foreignKey: 'proxy_id',
});
};
and
pool.associate = (models) => {
pool.belongsToMany(models.proxy, {
through: models.proxyPool,
foreignKey: 'pool_id',
});
};
When I call proxyPool.findOrCreate({where: {proxyId, poolId}}) it says that column proxyPool.proxyId does not exist, but in raw SQL I see:
SELECT "id", "pool_id" AS "poolId", "createdAt", "updatedAt", "pool_id", "proxy_id"
FROM "portnoi"."proxy_pool" AS "proxyPool"
WHERE "proxyPool"."proxyId" = '3' AND "proxyPool"."pool_id" = '1' LIMIT 1;
Why does it use alias for poolId = pool_id but not use alias described in model for proxyId = proxy_id?
Have you tried to make a query like that?
proxyPool.findOrCreate({
where: {
proxyId: {
[Op.eq]: proxyId,
},
...
}
})

Sequelize: on a subset of model A, sum an integer-attribute of an associated model B

I want to do this:
select sum("quantity") as "sum"
from "orderArticles"
inner join "orders"
on "orderArticles"."orderId"="orders"."id"
and "orderArticles"."discountTagId" = 2
and "orders"."paid" is not null;
which results in on my data base:
sum
-----
151
(1 row)
How can I do it?
My Sequelize solution:
The model definitions:
const order = Conn.define('orders', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true
},
// ...
paid: {
type: Sequelize.DATE,
defaultValue: null
},
// ...
},
// ...
})
const orderArticle = Conn.define('orderArticles',
{
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true
},
// ...
quantity: {
type: Sequelize.INTEGER,
defaultValue: 1
}
},
{
scopes: {
paidOrders: {
include: [
{ model: order, where: { paid: {$ne: null}} }
]
}
},
// ...
})
Associations:
orderArticle.belongsTo(order)
order.hasMany(orderArticle, {onDelete: 'cascade', hooks: true})
I came up with this after hours of research:
db.models.orderArticles
.scope('paidOrders') // select only orders with paid: {$ne: null}
.sum('quantity', { // sum up all resulting quantities
attributes: ['quantity'], // select only the orderArticles.quantity col
where: {discountTagId: 2}, // where orderArticles.discountTagId = 2
group: ['"order"."id"', '"orderArticles"."quantity"'] // don't know why, but Sequelize told me to
})
.then(sum => sum) // return the sum
leads to this sql:
SELECT "orderArticles"."quantity", sum("quantity") AS "sum",
"order"."id" AS "order.id", "order"."taxRate" AS "order.taxRate",
"order"."shippingCosts" AS "order.shippingCosts", "order"."discount"
AS "order.discount", "order"."paid" AS "order.paid",
"order"."dispatched" AS "order.dispatched", "order"."payday" AS
"order.payday", "order"."billNr" AS "order.billNr",
"order"."createdAt" AS "order.createdAt", "order"."updatedAt" AS
"order.updatedAt", "order"."orderCustomerId" AS
"order.orderCustomerId", "order"."billCustomerId" AS
"order.billCustomerId" FROM "orderArticles" AS "orderArticles" INNER
JOIN "orders" AS "order" ON "orderArticles"."orderId" = "order"."id"
AND "order"."paid" IS NOT NULL WHERE "orderArticles"."discountTagId" =
'4' GROUP BY "order"."id", "orderArticles"."quantity";
which has this result on the same data base: 0 rows
If you know what I got wrong please let me know!
Thank you :)
Found the solution:
in the scopes definition on the orderArticle model:
scopes: {
paidOrders: {
include: [{
model: order,
where: { paid: {$ne: null}},
attributes: [] // don't select additional colums!
}]
}
},
//...
and the algorithm:
db.models.orderArticles
.scope('paidOrders')
.sum('quantity', {
attributes: [], // don't select any further cols
where: {discountTagId: 2}
})
Note: In my case it was sufficient to return the promise. I use GraphQL which resolves the result and sends it to the client.

Using group by and joins in sequelize

I have two tables on a PostgreSQL database, contracts and payments. One contract has multiple payments done.
I'm having the two following models:
module.exports = function(sequelize, DataTypes) {
var contracts = sequelize.define('contracts', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true
}
}, {
createdAt: false,
updatedAt: false,
classMethods: {
associate: function(models) {
contracts.hasMany(models.payments, {
foreignKey: 'contract_id'
});
}
}
});
return contracts;
};
module.exports = function(sequelize, DataTypes) {
var payments = sequelize.define('payments', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true
},
contract_id: {
type: DataTypes.INTEGER,
},
payment_amount: DataTypes.INTEGER,
}, {
classMethods: {
associate: function(models) {
payments.belongsTo(models.contracts, {
foreignKey: 'contract_id'
});
}
}
});
return payments;
};
I would like to sum all the payments made for every contract, and used this function:
models.contracts.findAll({
attributes: [
'id'
],
include: [
{
model: models.payments,
attributes: [[models.sequelize.fn('sum', models.sequelize.col('payments.payment_amount')), 'total_cost']]
}
],
group: ['contracts.id']
})
But it generates the following query:
SELECT "contracts"."id", "payments"."id" AS "payments.id", sum("payments"."payment_amount") AS "payments.total_cost"
FROM "contracts" AS "contracts"
LEFT OUTER JOIN "payments" AS "payments" ON "contracts"."id" = "payments"."contract_id" GROUP BY "contracts"."id";
I do not ask to select payments.id, because I would have to include it in my aggregation or group by functions, as said in the error I have:
Possibly unhandled SequelizeDatabaseError: error: column "payments.id"
must appear in the GROUP BY clause or be used in an aggregate function
Am I missing something here ? I'm following this answer but even there I don't understand how the SQL request can be valid.
This issue has been fixed on Sequelize 3.0.1, the primary key of the included models must be excluded with
attributes: []
and the aggregation must be done on the main model (infos in this github issue).
Thus for my use case, the code is the following
models.contracts.findAll({
attributes: ['id', [models.sequelize.fn('sum', models.sequelize.col('payments.payment_amount')), 'total_cost']],
include: [
{
model: models.payments,
attributes: []
}
],
group: ['contracts.id']
})
Try
group: ['contracts.id', 'payments.id']
Can you write your function as
models.contracts.findAll({
attributes: [
'models.contracts.id'
],
include: [
{
model: models.payments,
attributes: [[models.sequelize.fn('sum', models.sequelize.col('payments.payment_amount')), 'total_cost']]
}
],
group: ['contracts.id']
})
Is the issue that you might want to be selecting from payments and joining contracts rather than the other way around?

Sails.js + Waterline: Many-to-Many through association

I'm new to Sails.js (v0.10.5) and Waterline ORM. I have 3 tables in database: users (id, name), roles(id, alias) and join table users_roles(user_id, role_id). It's important not to change table names and field names in database. I want Policy entity to be a join entity between User and Role. Here is some mapping code:
//User.js
module.exports = {
tableName: 'users',
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
id: {
type: 'integer',
required: true
},
name: {
type: 'string'
},
roles: {
collection: 'role',
via: 'users',
through: 'policy'
},
}
}
//Role.js
module.exports = {
tableName: "roles",
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
id: {
type: 'integer',
required: true
},
alias: {
type: 'string',
required: true
},
users: {
collection: 'user',
via: 'roles',
through: 'policy'
}
}
}
//Policy.js
module.exports = {
tableName: "users_roles",
tables: ['users', 'roles'],
junctionTable: true,
autoCreatedAt: false,
autoUpdatedAt: false,
attributes: {
user: {
columnName: 'user',
type: 'integer',
foreignKey: true,
references: 'user',
on: 'id',
via: 'role',
groupBy: 'user'
},
roles: {
columnName: 'role',
type: 'integer',
foreignKey: true,
references: 'role',
on: 'id',
via: 'user',
groupBy: 'role'
}
}
}
But when I trying to access roles atribute in controller
User.findOne({id: 1}).populate('roles').exec(function(err, user) {
console.log(JSON.stringify(user.roles));
});
this returns
[]
And
User.findOne({id: 1}).populate('roles').exec(function(err, user) {
console.log(JSON.stringify(user));
});
returns
{"id":1,"name":"test", "roles":[]}
I checked twice that user, role and association between them exists in database. What is my mistake?
I have found way to solve this problem. It's not what I exactly want, but it works.
First: join entity:
//Policy.js
module.exports = {
tableName: "users_roles",
autoPK: false,
attributes: {
id: {
type: 'integer',
primaryKey: true,
autoIncrement: true,
},
user: {
columnName: 'user_id',
model: 'user'
},
role: {
columnName: 'role_id',
model: 'role'
}
},
//tricky method to get all users for specified role_id
//or to get all roles for specified user_id
get: function(id, modificator, cb) {
var fields = ['user', 'role'];
if (fields.indexOf(modificator) < 0) {
cb(new Error('No such modificator in Policy.get()'), null);
}
var inversedField = fields[(fields.indexOf(modificator) + 1) % 2];
var condition = {};
condition[inversedField] = id;
this.find(condition).populate(modificator).exec(function(err, policies) {
if (err) {
cb(err, null);
return;
}
var result = [];
policies.forEach(function(policy) {
result.push(policy[modificator]);
});
cb(null, result);
return;
});
}
}
As you see, I added ID field to this entity (and to db table users_roles too), so it's not the great solution.
//User.js
module.exports = {
tableName: 'users',
autoPK: false,
attributes: {
id: {
type: 'integer',
primaryKey: true,
autoIncrement: true,
unique: true,
},
name: {
type: 'string'
},
policies: {
collection: 'policy',
via: 'user'
}
}
}
And Role Entity:
//Role.js
module.exports = {
tableName: 'roles',
autoPK: false,
attributes: {
id: {
type: 'integer',
primaryKey: true,
autoIncrement: true,
},
alias: {
type: 'string',
required: true,
unique: true,
},
policies: {
collection: 'policy',
via: 'role'
}
}
}
That's how I get all roles for specified user_id:
...
id = req.session.me.id; //user_id here
Policy.get(id, 'role', function(err, roles) {
var isAdmin = false;
roles.forEach(function(role) {
isAdmin |= (role.id === 1);
});
if (isAdmin) {
next(null);
return;
} else {
return res.redirect('/login');
}
});
...
Maybe it'll be usefull for somebody =)