PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

Kondisi sequence pada join table tidak bekerja dengan kondisi limit

Setelah sekitar satu minggu neraka menemukan solusi yang dapat diterima untuk kasus saya. Percayalah ini akan membantu karena menemukan banyak topik/masalah yang belum terjawab di github.

TL;DR; solusi sebenarnya ada di akhir postingan, hanya potongan kode terakhir.

Ide utamanya adalah Sequelize membangun query SQL yang benar, tetapi ketika meninggalkan join kita menghasilkan produk carthesian, sehingga akan ada banyak baris sebagai hasil query.

Contoh:Tabel A dan B. Relasi banyak ke banyak. Jika kita ingin semua A bergabung dengan B kita akan menerima baris A * B, jadi akan ada banyak baris untuk setiap record dari A dengan nilai yang berbeda dari 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

Dalam sintaks sekuel:

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’ }],
})

Semuanya berfungsi dengan baik.

Jadi, bayangkan saya ingin menerima 10 catatan dari A dengan memetakan ke mereka catatan dari B. Ketika kami menempatkan LIMIT 10 pada kueri ini, Sequelize membangun kueri yang benar tetapi LIMIT diterapkan ke seluruh kueri dan sebagai hasilnya kami hanya menerima 10 baris, di mana semua dari mereka hanya bisa untuk satu record dari A. Contoh:

A.findAll({
    distinct: true,
    include: [{ association: ‘ab’ }],
    limit: 10,
})

Yang akan diubah menjadi:

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

Setelah output diterima, Seruqlize sebagai ORM akan melakukan pemetaan data dan over query akan menghasilkan kode:

[
 {
  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 },
  ],
 }
]

Jelas TIDAK apa yang kita inginkan. Saya ingin menerima 10 catatan untuk A, tetapi hanya menerima 2, sementara saya tahu ada lebih banyak lagi di database.

Jadi kami memiliki kueri SQL yang benar tetapi masih menerima hasil yang salah.

Ok, saya punya beberapa ide tapi yang paling mudah dan paling logis adalah:1. Buat permintaan pertama dengan gabungan, dan kelompokkan hasil berdasarkan tabel sumber (tabel tempat kita membuat kueri dan yang membuat gabungan) properti 'id'. Sepertinya mudah.....

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. Sekarang kita akan menerima output yang benar hanya dengan id sumber daya yang diperlukan. Kemudian kita perlu membuat kueri seconf TANPA batas dan offset, tetapi tentukan klausa 'di mana' tambahan:
{
 ...,
 where: {
  ...,
  id: Sequelize.Op.in: [array of ids],
 }
}
  1. Dengan query tentang kita dapat menghasilkan query yang benar dengan LEFT JOINS.

Solusi Metode menerima model dan kueri asli sebagai argumen dan mengembalikan kueri yang benar + tambahan jumlah total catatan dalam DB untuk pagination. Itu juga mengurai urutan kueri dengan benar untuk menyediakan kemampuan untuk memesan berdasarkan bidang dari tabel yang digabungkan:

/**
   *  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. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Akses server Postgres jarak jauh dengan pgAdmin

  2. Bagaimana cara menghapus file temp yang tidak digunakan dari Postgres yang dikelola Google Cloud SQL?

  3. Sintaks yang benar untuk array tipe komposit

  4. Memilih array dalam format yang dicetak dengan baik di SQL

  5. analisis dimensi dan unit dalam database SQL