【问题标题】:Sequelize condition on joined table doesn't work with limit condition连接表上的 Sequelize 条件不适用于限制条件
【发布时间】:2019-10-03 21:17:42
【问题描述】:

我有一个 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 会发生什么?即

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 条件在连接发生之前放在子查询中,因此失败。

这里的正确方法是什么?

【问题讨论】:

  • 我测试了与hasOne() 关联的两个表的类似查询,添加limit 并没有重组查询。 SupplierCalendar加入什么样的协会?
  • 在本例中为hasMany。但我不明白为什么这很重要。使用原始 SQL,此查询非常简单 - 只需将 "calendars"."state" 上的 WHERE 条件移动到左外连接下方的外查询。 Sequelize 肯定可以做到这一点吗?
  • 它应该重要,但它确实......我用MySql测试过。本来建议你在 github 上将此作为错误记录下来,但我看到你已经这样做了。一个明显的解决方法Supplier.findAll(..).then (allResults =&gt; { let limitedResults = allResults.slice(0,10); ...
  • 解决方法完全绕过了数据库提供的内置限制功能。如果这是唯一可用的解决方案,那么 Sequelize 就完全没用了。
  • Sequelize 没有实现一些数据库功能,或者恕我直言,实现得不好。

标签: postgresql join subquery sequelize.js


【解决方案1】:

经过大约一周的地狱寻找我的案例可接受的解决方法。相信这会有所帮助,因为在 github 上发现了很多未解决的主题/问题。

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

收到输出后,Seruqlize as ORM 会进行数据映射,代码中的过度查询结果为:

[
 {
  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.
  1. 现在我们将正确接收仅包含必要资源 ID 的输出。然后我们需要编写没有限制和偏移量的 seconf 查询,但要指定额外的 'where' 子句:
{
 ...,
 where: {
  ...,
  id: Sequelize.Op.in: [array of ids],
 }
}
  1. 通过查询,我们可以使用 LEFT JOINS 生成正确的查询。

解决方案 方法接收模型和原始查询作为参数,并返回正确的查询+额外的数据库中的记录总数以进行分页。它还正确解析查询顺序,以提供按连接表中的字段排序的能力:

/**
   *  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));
  }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-07-15
    • 2017-06-12
    • 1970-01-01
    • 2015-07-05
    • 2016-12-19
    • 2023-04-08
    • 2019-11-21
    • 2012-08-10
    相关资源
    最近更新 更多