लगभग एक सप्ताह के नरक के बाद मेरे मामले के लिए स्वीकार्य समाधान मिला। विश्वास करें कि यह मददगार होगा क्योंकि जीथब पर बहुत सारे अनुत्तरित विषय/मुद्दे मिले हैं।
टीएल; डॉ; वास्तविक समाधान पोस्ट के अंत में है, बस कोड का अंतिम भाग है।
मुख्य विचार यह है कि Sequelize सही SQL क्वेरी बनाता है, लेकिन जब लेफ्ट जॉइन होता है तो हम कार्टेशियन उत्पाद का उत्पादन करते हैं, इसलिए क्वेरी परिणाम के रूप में बहुत सारी पंक्तियाँ होंगी।
उदाहरण:ए और बी टेबल। अनेक से अनेक संबंध। यदि हम सभी A को B से जोड़ना चाहते हैं तो हमें A * B पंक्तियाँ प्राप्त होंगी, इसलिए A से प्रत्येक रिकॉर्ड के लिए 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
सिक्वेलाइज़ सिंटैक्स में:
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’ }],
})
सब कुछ ठीक काम करता है।
तो, कल्पना कीजिए कि मैं ए से 10 रिकॉर्ड प्राप्त करना चाहता हूं जिसमें मैप किए गए बी से रिकॉर्ड हैं। जब हम इस क्वेरी पर LIMIT 10 डालते हैं, तो सीक्वेलाइज सही क्वेरी बनाते हैं लेकिन LIMIT पूरी क्वेरी पर लागू होता है और नतीजतन हमें केवल 10 पंक्तियां प्राप्त होती हैं, जहां सभी उनमें से ए से केवल एक रिकॉर्ड के लिए हो सकता है उदाहरण:
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 },
],
}
]
जाहिर है वह नहीं जो हम चाहते थे। मैं ए के लिए 10 रिकॉर्ड प्राप्त करना चाहता था, लेकिन केवल 2 प्राप्त किया, जबकि मुझे पता है कि डेटाबेस में और भी हैं।
तो हमारे पास सही SQL क्वेरी है लेकिन फिर भी गलत परिणाम प्राप्त हुआ है।
ठीक है, मेरे पास कुछ विचार थे लेकिन सबसे आसान और सबसे तार्किक है:1। जॉइन के साथ पहला अनुरोध करें, और सोर्स टेबल (जिस टेबल पर हम क्वेरी कर रहे हैं और जिससे जॉइन कर रहे हैं) 'आईडी' प्रॉपर्टी द्वारा ग्रुप रिजल्ट्स। आसान लगता है.....
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.
- अब हम केवल आवश्यक संसाधन आईडी के साथ सही ढंग से आउटपुट प्राप्त करेंगे। फिर हमें सीमा और ऑफसेट के बिना दूसरी क्वेरी बनाने की जरूरत है, लेकिन अतिरिक्त 'कहां' खंड निर्दिष्ट करें:
{
...,
where: {
...,
id: Sequelize.Op.in: [array of ids],
}
}
- के बारे में पूछताछ के साथ हम LEFT JOINS के साथ सही क्वेरी तैयार कर सकते हैं।
समाधान विधि मॉडल और मूल क्वेरी को तर्क के रूप में प्राप्त करती है और सही क्वेरी + अतिरिक्त रूप से पेजिनेशन के लिए डीबी में रिकॉर्ड की कुल राशि देता है। यह सम्मिलित तालिकाओं से फ़ील्ड द्वारा ऑर्डर करने की क्षमता प्रदान करने के लिए क्वेरी ऑर्डर को सही ढंग से पार्स भी करता है:
/**
* 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));
}