sequelize not Include all children 如果任何一个匹配

sequelize not Include all children if any one matches

我有三个背靠背的关联表。这意味着 item_level_1 有很多 item_level_2 而 item_level_2 有很多 item_level_3。我使用搜索查询来查找名称包含搜索文本的任何 parent 或 child。这意味着如果我输入 abc,那么我需要 return 所有 parent 或 child 以及完整的详细信息(parents 和 children) .但在我的例子中,如果 item_level_3 名称中有 abc,它 return 是 parent 的详细信息,但它只是 return 特定的 child 和来自 item_level_3 的 abc。我需要 return 所有 children inside item_level_3 where the same parent.

我在 AWS 中使用 MySQL 数据库和节点

我检查了 https://sequelize.org/master/manual/eager-loading.html#complex-where-clauses-at-the-top-level 并尝试了不同的组合。但无济于事。我可能会错过一些东西。但是我找不到。

exports.searchItems = (body) => {
    return new Promise((resolve, reject) => {
        let searchText = body.searchText.toLowerCase();
        let limit = body.limit;
        let offset = body.offset;
        
        db.item_level_1.findAndCountAll({
            where: {
                [Sequelize.Op.or]: [
                    Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_1.name")), Sequelize.Op.like, '%' + searchText + '%'),
                    Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_2.name")), Sequelize.Op.like, '%' + searchText + '%'),
                    Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_2.item_level_3.name")), Sequelize.Op.like, '%' + searchText + '%'),
                ],

                [Sequelize.Op.and]: [
                    Sequelize.where(Sequelize.col("item_level_1.status"), Sequelize.Op.eq, body.status)
                ]
            },
            offset: offset,
            limit: limit,
            distinct: true,
            subQuery: false,
            attributes: ['id', 'name'],
            include: [
                {
                    model: db.item_level_2,
                    as: 'item_level_2',
                    where: {
                        status: body.status
                    },
                    attributes: ['id', 'name'],
                    required: true,
                    include: [{
                        model: db.item_level_3,
                        as: 'item_level_3',
                        where: {
                            status: body.status
                        },
                        required: false,
                        attributes: ['id', 'name']
                    }]
                }
        ]
        }).then(result => {
            resolve({ [KEY_STATUS]: 1, [KEY_MESSAGE]: "items listed successfully", [KEY_DATA]: result.rows, [KEY_TOTAL_COUNT]: result.count });
        }).catch(error => {
            reject({ [KEY_STATUS]: 0, [KEY_MESSAGE]: "items list failed", [KEY_ERROR]: error });
        });
    })
}

预期结果

{
  "status": 1,
  "message": "Rent items listed successfully",
  "data": [
    {
      "id": 21,
      "name": "this is test parent one",
      "item_level_2": [
        {
          "id": 39,
          "name": "this is second test parent one",
          "item_level_3": {
            "id": 9,
            "name": "this is the child description with abc"
          }
        },
        {
          "id": 40,
          "name": "this is second test parent two",
          "item_level_3": {
            "id": 6,
            "name": "this is the child description with def"
          }
        },
        {
          "id": 41,
          "name": "this is second test parent three",
          "item_level_3": {
            "id": 70,
            "name": "this is the child description with ghi"
          }
        }
      ]
    }
  ],
  "totalCount": 1
}

实际结果

{
  "status": 1,
  "message": "Rent items listed successfully",
  "data": [
    {
      "id": 21,
      "name": "this is test parent one",
      "item_level_2": [
        {
          "id": 39,
          "name": "this is second test parent one",
          "item_level_3": {
            "id": 9,
            "name": "this is the child description with abc"
          }
        }
      ]
    }
  ],
  "totalCount": 1
}

item_level_1 型号

module.exports = (sequelize, DataTypes) => {
    const item_level_1 = sequelize.define("item_level_1", {
        id: { type: INTEGER, primaryKey: true, autoIncrement: true },
        name: { type: STRING },
        status: { type: BOOLEAN, defaultValue: 0 }
    }, {
        timestamps: false,
        freezeTableName: true,
    })
    item_level_1.associate = function (models) {
        item_level_1.hasMany(models.item_level_2, { as: 'item_level_2' });
    };
    return item_level_1;

}

item_level_2 型号

module.exports = (sequelize, DataTypes) => {
    const item_level_2 = sequelize.define("item_level_2", {
        id: { type: INTEGER, primaryKey: true, autoIncrement: true },
        name: { type: STRING },
        status: { type: BOOLEAN, defaultValue: 0 },
        itemLevel2Id: { type: INTEGER },
        itemLevel1Id: { type: INTEGER }
    }, {
        timestamps: false,
        freezeTableName: true,
    })
    item_level_2.associate = function (models) {
        item_level_2.belongsTo(models.item_level_3, { as: 'item_level_3', foreignKey: 'itemLevel2Id' });
    };
    return item_level_2;

}

item_level_2 型号

module.exports = (sequelize, DataTypes) => {
    const item_level_3 = sequelize.define("item_level_3", {
        id: { type: INTEGER, primaryKey: true, autoIncrement: true },
        name: { type: STRING },
        status: { type: BOOLEAN, defaultValue: 0 }
    }, {
        timestamps: false,
        freezeTableName: true,
    })
    return item_level_3;

}

不幸的是,我认为子查询是不可避免的。您需要先从匹配的 lvl_3 项中找到 lvl_2 个 ID。

const itemsLevel2 = await db.item_level_2.findAll(
    {           
        attributes: [Sequelize.col("item_level_2.id"), 'id2'],
        where: 
        {[Sequelize.Op.and]: [
            Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_2.item_level_3.name")), Sequelize.Op.like, '%' + searchText + '%'), 
            Sequelize.where(Sequelize.col("item_level_2.status"), Sequelize.Op.eq, body.status)
        ]},
        include: [{
            model: db.item_level_3,
            as: 'item_level_3',
            where: {
                status: body.status
            },
            required: true,
            attributes: ['name']
        }]
    }
)
ids = itemsLevel2.map(item => item.id);

然后像这样使用所需的 ID:

exports.searchItems = (body) => {
    return new Promise((resolve, reject) => {
        let searchText = body.searchText.toLowerCase();
        let limit = body.limit;
        let offset = body.offset;
        
        db.item_level_1.findAndCountAll({
            where: {
                [Sequelize.Op.or]: [
                    Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_1.name")), Sequelize.Op.like, '%' + searchText + '%'),
                    Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_2.name")), Sequelize.Op.like, '%' + searchText + '%'),
                    Sequelize.where(Sequelize.col("item_level_2.id"), Sequelize.Op.in, ids),
                ],

                [Sequelize.Op.and]: [
                    Sequelize.where(Sequelize.col("item_level_1.status"), Sequelize.Op.eq, body.status)
                ]
            },
            offset: offset,
            limit: limit,
            distinct: true,
            subQuery: false,
            attributes: ['id', 'name'],
            include: [
                {
                    model: db.item_level_2,
                    as: 'item_level_2',
                    where: {
                        status: body.status
                    },
                    attributes: ['id', 'name'],
                    required: true,
                    include: [{
                        model: db.item_level_3,
                        as: 'item_level_3',
                        where: {
                            status: body.status
                        },
                        required: true,
                        attributes: ['id', 'name']
                    }]
                }
        ]
        }).then(result => {
            resolve({ [KEY_STATUS]: 1, [KEY_MESSAGE]: "items listed successfully", [KEY_DATA]: result.rows, [KEY_TOTAL_COUNT]: result.count });
        }).catch(error => {
            reject({ [KEY_STATUS]: 0, [KEY_MESSAGE]: "items list failed", [KEY_ERROR]: error });
        });
    })
}

这是一个复杂的场景,需要一些解决方法。另外,我还没有测试所有场景,很抱歉它可能适用于示例案例,但不能满足您的所有需求。不过,我希望这能给你一些指导。

基于这里写的SQL,https://dba.stackexchange.com/a/140006,你可以在item_level_2item_level_3之间创建2个JOIN,1个用于过滤,1个用于获取所有关联记录.

item_level_2.hasMany(item_level_3, { as: 'item_level_3' });
// This extra association will be used only for filtering.
item_level_2.hasMany(item_level_3, { as: 'filter' }); 

然后,

db.item_level_1.findAndCountAll({
    where: {
        [Sequelize.Op.or]: [
             Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_1.name")), Sequelize.Op.like, '%' + searchText + '%'),
             Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_2.name")), Sequelize.Op.like, '%' + searchText + '%'),
             // Use the filter association to filter data.
             Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_2.filter.name")), Sequelize.Op.like, '%' + searchText + '%'),
        ],
        ...
        include: [
            {
                model: db.item_level_2,
                as: 'item_level_2',
                where: {
                    status: body.status
                },
                attributes: ['id', 'name'],
                required: true,
                include: [
                    {
                        model: db.item_level_3,
                        as: 'item_level_3',
                        where: {
                            status: body.status
                        },
                        required: false,
                        attributes: ['id', 'name']  // This should fetch all associated data. 
                    },
                    {
                        model: db.item_level_3,
                        as: 'filter',
                        where: {
                            status: body.status
                        },
                        required: false,
                        attributes: []  // Do not fetch any data from this association. This is only for filtering.
                    }
                ]
            }
        ]
    }
})

这涵盖了 1 个项目与 item_level_3 匹配并且有多个项目与相同的 item_level_2 相关联的场景。如果有多个 item_level_2item_level_1 相关联并且 item_level_2 中的 1 个与 searchText 匹配,这将不起作用。

我还没有测试过,但是,如果您需要,也许您也可以为 item_level_1 做类似的事情。

=========================================== ====

更新:

如果item_level_2item_level_3之间的关联是belongsTo,上述解决方案将不起作用。

您需要 WHERE EXISTS 查询 item_level_3 (省略错误解)

=========================================== ====

更新2:

使用内联 IN 查询进行 item_level_3 文本匹配。

在进行内联查询之前,确保转义稍后将进入 Sequelize.literal 的动态内容。

Important Note: Since sequelize.literal inserts arbitrary content without escaping to the query, it deserves very special attention since it may be a source of (major) security vulnerabilities. It should not be used on user-generated content.

参考:https://sequelize.org/master/manual/sub-queries.html

const escapedSearchText = sequelize.escape(`%${searchText}%`);

首先设置内联查询选项以提取 item_level_1 的 ID,其中 searchText 出现在任何子项 (item_level_3) 中。为此,我仅查询 item_level_2item_level_3 表并使用 GROUPHAVING.

const inQueryOptions = {
    attributes: ['itemLevel1Id'],  // This attribute name and the one in group could be different for your table.
    include: [{
        attributes: [],
        model: db.item_level_3,
        as: 'item_level_3',
        where: {
            name: {
                [Sequelize.Op.like]: escapedSearchText
            }
        }
    }],
    group: 'itemLevel1Id',
    having: Sequelize.literal('COUNT(*) > 0')
};

使用 item_level_1 的 id 进行分组并使用 HAVING 进行过滤,这将 return 所有 item_level_1 的 id,其中其任何子项位于 [=18] =] 有 searchText.

这仍然只搜索 item_level_3 的名字。

接下来,将选项转换为内联查询。

const Model = require("sequelize/lib/model");
// This is required when the inline query has `include` options, this 1 line make sure to serialize the query correctly.
Model._validateIncludedElements.bind(db.item_level_2)(inQueryOptions);
  
// Then, pass the query options to queryGenerator.
// slice(0, -1) is to remove the last ";" as I will use this query inline of the main query.
const inQuery = db.sequelize.getQueryInterface().queryGenerator.selectQuery('item_level_2', inQueryOptions, db.item_level_2).slice(0, -1);

生成的 inQuery 看起来像这样。

SELECT `item_level_2`.`itemLevel1Id` 
FROM `item_level_2` AS `item_level_2` 
INNER JOIN `item_level_3` AS `item_level_3` 
    ON `item_level_2`.`itemLevel3Id` = `item_level_3`.`id` 
    AND `item_level_3`.`name` LIKE '%def%' 
GROUP BY `itemLevel1Id` 
HAVING COUNT(*) > 0

最后,将这个生成的查询插入到主查询中。

db.item_level_1.findAndCountAll({
    subQuery: false,
    distinct: true,
    where: {
        [Op.or]: [
            Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_1.name")), Sequelize.Op.like, '%' + searchText + '%'),
            Sequelize.where(Sequelize.fn('lower', Sequelize.col("item_level_2.name")), Sequelize.Op.like, '%' + searchText + '%'),
            {
                id: {
                    // This is where I am inserting the inline query.
                    [Op.in]: Sequelize.literal(`(${inQuery})`)
                }
            }
        ]
    },
    attributes: ['id', 'name'],
    include: [{
        attributes: ['id', 'name'],
        model: db.item_level_2,
        as: 'item_level_2',
        required: true,
        include: [{
            attributes: ['id', 'name'],
            model: db.item_level_3,
            as: 'item_level_3',
            required: false,
        }]
    }]
});