postgresql - 连接表上的 Sequelize 条件不适用于限制条件

标签 postgresql join subquery sequelize.js

我有一个带有关联 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.
    
  • 现在我们将只接收到必要的资源 ID 的正确输出。然后我们需要在没有限制和偏移的情况下编写 seconf 查询,但指定额外的 'where' 子句:
  • {
     ...,
     where: {
      ...,
      id: Sequelize.Op.in: [array of ids],
     }
    }
    
  • 查询 about 我们可以使用 LEFT JOINS 生成正确的查询。

  • 解决方案
    方法接收模型和原始查询作为参数,并返回正确的查询 + 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/

    相关文章:

    mysql - SQL查询中子查询的语法和逻辑

    mysql - 如果 2 个表具有相同的数据,我如何比较?

    sql - 添加 WHERE 约束后,Postgresql 查询速度莫名其妙地变慢

    MySQL 合并两个表的结果

    mysql - 添加两个子查询以生成第三列

    SQL:如何获取表的每个最大值?

    mysql LAST_DAY()只读取1个子查询结果,如何处理所有结果?使用连接?

    postgresql - 如何使用 spring-data-jdbc 读/写 postgres jsonb 类型?

    postgresql - 如何使用 "pg_dump"实用程序转储数据时排除表分区

    mysql - 如何对 SQL 中允许特定组计数为零的字段进行计数?