我有一个带有关联 Supplier
模型的 Calendar
模型。
我想找供应商
我可以使用以下方法执行此操作:
Supplier.findAll({
include: [
{
model: Calendar,
as: 'calendars',
required: false,
where: {
start_time: { [Op.lte]: date },
end_time: { [Op.gte]: date },
},
},
],
where: {
'$calendars.state$': {
[Op.or]: [
{ [Op.in]: ['available'] },
{ [Op.eq]: null },
],
},
},
});
这会生成以下 SQL(删除了不相关的列):
SELECT
"suppliers"."uuid"
,"calendars"."uuid" AS "calendars.uuid"
,"calendars"."state" AS "calendars.state"
FROM "suppliers" AS "suppliers"
LEFT OUTER JOIN "suppliers_calendars" AS "calendars" ON
"suppliers"."uuid" = "calendars"."supplier_id"
AND "calendars"."start_time" <= '2019-05-27 23:00:00.000 +00:00'
AND "calendars"."end_time" >= '2019-05-27 23:00:00.000 +00:00'
WHERE (
("calendars"."state" IN ('available')
OR "calendars"."state" IS NULL
)
)
ORDER BY "suppliers"."uuid"
;
酷,正如预期的那样。现在如果我添加
limit
会发生什么? IE。Supplier.findAll({
include: [
{
model: Calendar,
as: 'calendars',
required: false,
where: {
start_time: { [Op.lte]: date },
end_time: { [Op.gte]: date },
},
},
],
where: {
'$calendars.state$': {
[Op.or]: [
{ [Op.in]: ['available'] },
{ [Op.eq]: null },
],
},
},
limit: 10,
});
这会产生以下结果:
SELECT
"suppliers".*
,"calendars"."uuid" AS "calendars.uuid"
,"calendars"."state" AS "calendars.state"
FROM (
SELECT "suppliers"."uuid"
FROM "suppliers" AS "suppliers"
WHERE (
("calendars"."state" IN ('available')
OR "calendars"."state" IS NULL)
)
ORDER BY "suppliers"."uuid"
LIMIT 10
) AS "suppliers"
LEFT OUTER JOIN "suppliers_calendars" AS "calendars" ON
"suppliers"."uuid" = "calendars"."supplier_id"
AND "calendars"."start_time" <= '2019-05-27 23:00:00.000 +00:00'
AND "calendars"."end_time" >= '2019-05-27 23:00:00.000 +00:00'
ORDER BY "suppliers"."uuid"
这是一个完全不同的查询,主要部分放在子查询中,连接放在外面。但是连接表上的
where
条件在连接发生之前放在子查询中,因此失败。这里的正确方法是什么?
最佳答案
经过大约一周的 hell 后,我的案例找到了可接受的解决方法。相信它会有所帮助,因为在 github 上发现了许多 Unresolved 主题/问题。
TL;DR;实际的解决方案在帖子的末尾,只是最后一段代码。
主要思想是 Sequelize 构建正确的 SQL 查询,但是当有左连接时,我们会生成 Carthesian 乘积,因此查询结果将有很多行。
示例:A 和 B 表。多对多关系。如果我们想让所有 A 与 B 连接,我们将收到 A * B 行,因此 A 中的每条记录都会有很多行与 B 中的值不同。
CREATE TABLE IF NOT EXISTS a (
id INTEGER PRIMARY KEY NOT NULL,
title VARCHAR
)
CREATE TABLE IF NOT EXISTS b (
id INTEGER PRIMARY KEY NOT NULL,
age INTEGER
)
CREATE TABLE IF NOT EXISTS ab (
id INTEGER PRIMARY KEY NOT NULL,
aid INTEGER,
bid INTEGER
)
SELECT *
FROM a
LEFT JOIN (ab JOIN b ON b.id = ab.bid) ON a.id = ab.aid
在 Sequelize 语法中:
class A extends Model {}
A.init({
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
title: {
type: Sequelize.STRING,
},
});
class B extends Model {}
B.init({
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
age: {
type: Sequelize.INTEGER,
},
});
A.belongsToMany(B, { foreignKey: ‘aid’, otherKey: ‘bid’, as: ‘ab’ });
B.belongsToMany(A, { foreignKey: ‘bid’, otherKey: ‘aid’, as: ‘ab’ });
A.findAll({
distinct: true,
include: [{ association: ‘ab’ }],
})
一切正常。
所以,假设我想从 A 接收 10 条记录,并映射到来自 B 的记录。
当我们在此查询上设置 LIMIT 10 时,Sequelize 构建正确的查询,但 LIMIT 应用于整个查询,因此我们只收到 10 行,其中所有行都可能仅用于 A 中的一条记录。示例:
A.findAll({
distinct: true,
include: [{ association: ‘ab’ }],
limit: 10,
})
将转换为:
SELECT *
FROM a
LEFT JOIN (ab JOIN b ON b.id = ab.bid) ON a.id = ab.aid
LIMIT 10
id | title | id | aid | bid | id | age
--- | -------- | ----- | ----- | ----- | ----- | -----
1 | first | 1 | 1 | 1 | 1 | 1
1 | first | 2 | 1 | 2 | 2 | 2
1 | first | 3 | 1 | 3 | 3 | 3
1 | first | 4 | 1 | 4 | 4 | 4
1 | first | 5 | 1 | 5 | 5 | 5
2 | second | 6 | 2 | 5 | 5 | 5
2 | second | 7 | 2 | 4 | 4 | 4
2 | second | 8 | 2 | 3 | 3 | 3
2 | second | 9 | 2 | 2 | 2 | 2
2 | second | 10 | 2 | 1 | 1 | 1
收到输出后,作为ORM的Seruqlize会进行数据映射,代码中的over查询结果为:
[
{
id: 1,
title: 'first',
ab: [
{ id: 1, age:1 },
{ id: 2, age:2 },
{ id: 3, age:3 },
{ id: 4, age:4 },
{ id: 5, age:5 },
],
},
{
id: 2,
title: 'second',
ab: [
{ id: 5, age:5 },
{ id: 4, age:4 },
{ id: 3, age:3 },
{ id: 2, age:2 },
{ id: 1, age:1 },
],
}
]
显然不是我们想要的。我想收到 A 的 10 条记录,但只收到了 2 条,而我知道数据库中有更多记录。
所以我们有正确的 SQL 查询,但仍然收到不正确的结果。
好的,我有一些想法,但最简单和最合乎逻辑的是:
1. 使用连接发出第一个请求,并按源表(我们正在查询和连接的表)'id' 属性对结果进行分组。看起来很简单.....
To make so we need to provide 'group' property to Sequelize query options. Here we have some problems. First - Sequelize makes aliases for each table while generating SQL query. Second - Sequelize puts all columns from JOINED table into SELECT statement of its query and passing __'attributes' = []__ won't help. In both cases we'll receive SQL error.
To solve first we need to convert Model.tableName to singluar form of this word (this logic is based on Sequelize). Just use [pluralize.singular()](https://www.npmjs.com/package/pluralize#usage). Then compose correct property to GROUP BY:
```ts
const tableAlias = pluralize.singular('Industries') // Industry
{
...,
group: [`${tableAlias}.id`]
}
```
To solve second (it was the hardest and the most ... undocumented). We need to use undocumented property 'includeIgnoreAttributes' = false. This will remove all columns from SELECT statement unless we specify some manually. We should manually specify attributes = ['id'] on root query.
{
...,
where: {
...,
id: Sequelize.Op.in: [array of ids],
}
}
解决方案
方法接收模型和原始查询作为参数,并返回正确的查询 + DB 中用于分页的额外记录总数。它还正确解析查询顺序以提供按连接表中的字段排序的能力:
/**
* Workaround for Sequelize illogical behavior when querying with LEFT JOINS and having LIMIT / OFFSET
*
* Here we group by 'id' prop of main (source) model, abd using undocumented 'includeIgnoreAttributes'
* Sequelize prop (it is used in its static count() method) in order to get correct SQL request
* Witout usage of 'includeIgnoreAttributes' there are a lot of extra invalid columns in SELECT statement
*
* Incorrect example without 'includeIgnoreAttributes'. Here we will get correct SQL query
* BUT useless according to business logic:
*
* SELECT "Media"."id", "Solutions->MediaSolutions"."mediaId", "Industries->MediaIndustries"."mediaId",...,
* FROM "Medias" AS "Media"
* LEFT JOIN ...
* WHERE ...
* GROUP BY "Media"."id"
* ORDER BY ...
* LIMIT ...
* OFFSET ...
*
* Correct example with 'includeIgnoreAttributes':
*
* SELECT "Media"."id"
* FROM "Medias" AS "Media"
* LEFT JOIN ...
* WHERE ...
* GROUP BY "Media"."id"
* ORDER BY ...
* LIMIT ...
* OFFSET ...
*
* @param model - Source model (necessary for getting its tableName for GROUP BY option)
* @param query - Parsed and ready to use query object
*/
private async fixSequeliseQueryWithLeftJoins<C extends Model>(
model: ModelCtor<C>, query: FindAndCountOptions,
): IMsgPromise<{ query: FindAndCountOptions; total?: number }> {
const fixedQuery: FindAndCountOptions = { ...query };
// If there is only Tenant data joined -> return original query
if (query.include && query.include.length === 1 && (query.include[0] as IncludeOptions).model === Tenant) {
return msg.ok({ query: fixedQuery });
}
// Here we need to put it to singular form,
// because Sequelize gets singular form for models AS aliases in SQL query
const modelAlias = singular(model.tableName);
const firstQuery = {
...fixedQuery,
group: [`${modelAlias}.id`],
attributes: ['id'],
raw: true,
includeIgnoreAttributes: false,
logging: true,
};
// Ordering by joined table column - when ordering by joined data need to add it into the group
if (Array.isArray(firstQuery.order)) {
firstQuery.order.forEach((item) => {
if ((item as GenericObject).length === 2) {
firstQuery.group.push(`${modelAlias}.${(item as GenericObject)[0]}`);
} else if ((item as GenericObject).length === 3) {
firstQuery.group.push(`${(item as GenericObject)[0]}.${(item as GenericObject)[1]}`);
}
});
}
return model.findAndCountAll<C>(firstQuery)
.then((ids) => {
if (ids && ids.rows && ids.rows.length) {
fixedQuery.where = {
...fixedQuery.where,
id: {
[Op.in]: ids.rows.map((item: GenericObject) => item.id),
},
};
delete fixedQuery.limit;
delete fixedQuery.offset;
}
/* eslint-disable-next-line */
const total = (ids.count as any).length || ids.count;
return msg.ok({ query: fixedQuery, total });
})
.catch((err) => this.createCustomError(err));
}
关于postgresql - 连接表上的 Sequelize 条件不适用于限制条件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56171187/