PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Warunek sekwencjonowania na połączonym stole nie działa z warunkiem limitu

Po około tygodniu piekła znalazłem akceptowalne obejście dla mojej sprawy. Uwierz, że byłoby to pomocne, ponieważ znalazłem wiele tematów/problemów bez odpowiedzi na github.

TL;DR; rzeczywiste rozwiązanie znajduje się na końcu posta, tylko ostatni fragment kodu.

Główną ideą jest to, że Sequelize buduje poprawne zapytanie SQL, ale po pozostawieniu lewych złączeń tworzymy iloczyn kartezjański, więc wynik zapytania będzie zawierał wiele wierszy.

Przykład:tabele A i B. Relacja „wiele do wielu”. Jeśli chcemy połączyć wszystkie A z B, otrzymamy wiersze A * B, więc dla każdego rekordu z A będzie wiele wierszy z różnymi wartościami z 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

W składni sequeli:

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

Wszystko działa dobrze.

Wyobraźmy sobie więc, że chcę otrzymać 10 rekordów od A z przypisanymi do nich rekordami od B. Gdy wstawimy LIMIT 10 na to zapytanie, Sequelize zbuduje poprawne zapytanie, ale LIMIT zostanie zastosowany do całego zapytania i w rezultacie otrzymamy tylko 10 wierszy, w których wszystkie z nich może dotyczyć tylko jednego rekordu z A. Przykład:

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

Które zostaną przekonwertowane na:

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

Po otrzymaniu danych wyjściowych Seruqlize jako ORM dokona mapowania danych, a wynik zapytania w kodzie będzie następujący:

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

Oczywiście NIE to, czego chcieliśmy. Chciałem otrzymać 10 rekordów dla A, ale otrzymałem tylko 2, podczas gdy wiem, że w bazie danych jest ich więcej.

Czyli mamy poprawne zapytanie SQL, ale nadal otrzymaliśmy niepoprawny wynik.

Ok, miałem kilka pomysłów, ale najprostszy i najbardziej logiczny to:1. Wykonaj pierwsze żądanie ze złączeniami i pogrupuj wyniki według tabeli źródłowej (tabeli, na której wykonujemy zapytanie i do której dokonujemy złączeń) właściwość 'id'. Wydaje się łatwe.....

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. Teraz otrzymamy poprawnie dane wyjściowe z tylko niezbędnymi identyfikatorami zasobów. Następnie musimy utworzyć zapytanie seconf BEZ limitu i offsetu, ale podać dodatkową klauzulę 'where':
{
 ...,
 where: {
  ...,
  id: Sequelize.Op.in: [array of ids],
 }
}
  1. Dzięki zapytaniu o możemy wygenerować poprawne zapytanie za pomocą LEFT JOINS.

Rozwiązanie Metoda otrzymuje model oraz oryginalne zapytanie jako argumenty i zwraca poprawne zapytanie + dodatkowo całkowitą ilość rekordów w DB do paginacji. Poprawnie analizuje również kolejność zapytań, aby zapewnić możliwość porządkowania według pól z połączonych tabel:

/**
   *  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. Jak mogę uzyskać listę wszystkich funkcji przechowywanych w bazie danych określonego schematu w PostgreSQL?

  2. Nie można utworzyć sterownika z NHibernate.Driver.NpgsqlDriver

  3. Poprawa wydajności zapytań jsonb Postgres w połączeniu z zapytaniami relacyjnymi

  4. Uzyskaj wszystkie klucze obce za pomocą JDBC

  5. Uruchom kod rails po zatwierdzeniu aktualizacji bazy danych, bez after_commit