sql >> Databasteknik >  >> RDS >> PostgreSQL

Uppföljningsvillkor på sammanfogat bord fungerar inte med limitvillkor

Efter ungefär en veckas helvete hittade jag en acceptabel lösning för mitt fall. Tror det skulle vara till hjälp eftersom man hittade många obesvarade ämnen/problem på github.

TL;DR; den faktiska lösningen är i slutet av inlägget, bara den sista koden.

Huvudtanken är att Sequelize bygger korrekt SQL-fråga, men när vi har lämnat joins producerar vi kartesiska produkter, så det kommer att bli många rader som frågeresultat.

Exempel:A- och B-tabeller. Många till många relation. Om vi ​​vill få alla A sammanfogade med B kommer vi att få A * B-rader, så det kommer att finnas många rader för varje post från A med olika värden från 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

I uppföljningssyntax:

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

Allt fungerar ok.

Så tänk dig att jag vill ta emot 10 poster från A med mappade till dem poster från B. När vi sätter LIMIT 10 på den här frågan bygger Sequelize korrekt fråga men LIMIT tillämpas på hela frågan och som ett resultat får vi bara 10 rader , där alla av dem kan endast vara för en post från A. Exempel:

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

Som kommer att omvandlas till:

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

Efter att utdata har tagits emot kommer Seruqlize as ORM att göra datamappning och överfrågningsresultat i koden blir:

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

Uppenbarligen INTE vad vi ville ha. Jag ville ta emot 10 poster för A, men fick bara 2, medan jag vet att det finns fler i databasen.

Så vi har korrekt SQL-fråga men fick fortfarande ett felaktigt resultat.

Ok, jag hade några idéer men det enklaste och mest logiska är:1. Gör en första begäran med joins och gruppera resultat efter källtabell (tabell där vi gör en fråga och till vilken gör joins) egenskapen 'id'. Verkar lätt.....

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. Nu kommer vi att ta emot korrekt utdata med endast nödvändiga resurs-ID. Sedan måste vi komponera seconf-fråga UTAN gräns och offset, men ange ytterligare 'where'-sats:
{
 ...,
 where: {
  ...,
  id: Sequelize.Op.in: [array of ids],
 }
}
  1. Med fråga om kan vi skapa korrekta frågor med LEFT JOINS.

Lösning Metoden tar emot modell och originalfråga som argument och returnerar korrekt fråga + dessutom total mängd poster i DB för sidnumrering. Den analyserar också frågeordningen korrekt för att ge möjlighet att sortera efter fält från sammanfogade tabeller:

/**
   *  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. vad betyder Not a PersistText-värde?

  2. MySQL avaktiverad i Lampp, Xampp på Linux 12.04

  3. mysql_ping hänger med Amazon RDS

  4. Konfigurera Django med GeoDjango Support i AWS Beanstalk eller EC2 Instance