sql >> データベース >  >> RDS >> PostgreSQL

結合されたテーブルの続編条件が制限条件で機能しない

    地獄の約1週間後、私の場合は許容できる回避策が見つかりました。 githubで未回答のトピックや問題がたくさん見つかったので、役立つと思います。

    TL; DR;実際の解決策は投稿の最後にあり、コードの最後の部分にすぎません。

    主なアイデアは、Sequelizeが正しいSQLクエリを作成することですが、結合を残した場合、カルテシアン積を生成するため、クエリ結果として多くの行があります。

    例:AテーブルとBテーブル。多対多の関係。すべてのAをBと結合したい場合は、A * B行を受け取るので、Bとは異なる値を持つAからの各レコードに多くの行があります。

    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
    

    構文の続編:

    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からの1つのレコードのみに対するものである可能性があります。例:

    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は次のようになります。

    [
     {
      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. クエリを使用すると、LEFTJOINSを使用して正しいクエリを生成できます。

    解決策 メソッドは、モデルと元のクエリを引数として受け取り、正しいクエリと、ページ付けのために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));
      }
    



    1. 1つの列の値に基づいて重複する行を削除します

    2. Ionicフレームワークとphpmysql

    3. OracleのHEXTORAW()関数

    4. OracleWITH句はデータを返しません