PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Sequelize-Bedingung für verknüpfte Tabelle funktioniert nicht mit Limit-Bedingung

Nach ungefähr einer Woche fand die Hölle einen akzeptablen Workaround für meinen Fall. Glauben Sie, dass es hilfreich wäre, da Sie viele unbeantwortete Themen/Probleme auf GitHub gefunden haben.

TL;DR; Die eigentliche Lösung befindet sich am Ende des Beitrags, nur das letzte Stück Code.

Die Hauptidee ist, dass Sequelize eine korrekte SQL-Abfrage erstellt, aber wenn wir linke Verknüpfungen haben, erzeugen wir ein karthesisches Produkt, so dass es viele Zeilen als Abfrageergebnis gibt.

Beispiel:A- und B-Tabellen. Viele-zu-viele-Beziehung. Wenn wir alle A mit B verbinden wollen, erhalten wir A * B-Zeilen, also gibt es viele Zeilen für jeden Datensatz von A mit unterschiedlichen Werten von 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

In Fortsetzungssyntax:

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

Alles funktioniert einwandfrei.

Stellen Sie sich also vor, ich möchte 10 Datensätze von A mit zugeordneten Datensätzen von B erhalten. Wenn wir LIMIT 10 auf diese Abfrage setzen, erstellen Sie die korrekte Abfrage sequelize, aber LIMIT wird auf die gesamte Abfrage angewendet, und als Ergebnis erhalten wir nur 10 Zeilen , wo alle davon könnte nur für einen Datensatz von A gelten. Beispiel:

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

Was umgewandelt wird in:

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

Nachdem die Ausgabe empfangen wurde, führt Seruqlize als ORM eine Datenzuordnung durch und das Ergebnis der Abfrage im Code lautet:

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

Offensichtlich NICHT das, was wir wollten. Ich wollte 10 Datensätze für A erhalten, habe aber nur 2 erhalten, obwohl ich weiß, dass es mehr in der Datenbank gibt.

Wir haben also eine korrekte SQL-Abfrage, aber immer noch ein falsches Ergebnis erhalten.

Ok, ich hatte einige Ideen, aber die einfachste und logischste ist:1. Stellen Sie die erste Anfrage mit Verknüpfungen und gruppieren Sie die Ergebnisse nach Quelltabelle (Tabelle, auf der wir Abfragen durchführen und zu der Verknüpfungen vorgenommen werden) 'id'-Eigenschaft. Scheint einfach.....

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. Jetzt erhalten wir eine korrekte Ausgabe mit nur den notwendigen Ressourcen-IDs. Dann müssen wir eine seconf-Abfrage OHNE Limit und Offset erstellen, aber eine zusätzliche 'where'-Klausel angeben:
{
 ...,
 where: {
  ...,
  id: Sequelize.Op.in: [array of ids],
 }
}
  1. Mit der Abfrage über können wir mit LEFT JOINS eine korrekte Abfrage erzeugen.

Lösung Methode erhält Modell- und Originalabfrage als Argumente und gibt korrekte Abfrage + zusätzlich Gesamtzahl der Datensätze in der DB für die Paginierung zurück. Es analysiert auch die Abfragereihenfolge korrekt, um die Möglichkeit zu bieten, nach Feldern aus verknüpften Tabellen zu sortieren:

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