Sequelize:按关联数量/多个关联筛选查询

Sequelize: Filter query by amount of / multiple associations

提问人:JaRoMaster 提问时间:11/15/2023 最后编辑:JaRoMaster 更新时间:11/20/2023 访问量:116

问:

赏金将在 3 天后到期。这个问题的答案有资格获得 +200 声望赏金。JaRoMaster希望引起人们对这个问题的更多关注
希望看到一个智能解决方案来实现这个(可选)过滤逻辑。先谢谢你

我在 sequelize 中有两个模型:modelA 和 modelB。 这些模型处于多对多关系中。

我想获取 modelA 的所有实体,这些实体被关联到 modelB 的(多个)特定实例。

例如,我想获取与 id 为 (1, 3, 4) 的 modelB 相关的所有 modelA 模型。我想让每个模型至少与所有指定的模型B有关系。

如何在续集中实现这一点?

这是我到目前为止尝试过的:

模型 1:

@Table
export class Workout extends Model<Partial<Workout>> {
    @UniqueIndex
    @Column
    name!: string;

    @Column
    length!: number;

    @UniqueIndex
    @ForeignKey(() => User)
    @Column
    userId?: number;

    @BelongsTo(() => User, "userId")
    user?: User;

    @BelongsToMany(() => Tag, () => WorkoutTag)
    tags?: Tag[];
}

模型 2:

@Table({ timestamps: false })
export class Tag extends Model<Partial<Tag>> {
    @Unique
    @Column
    name!: string;

    @BelongsToMany(() => Workout, () => WorkoutTag)
    workouts?: Workout[];
}

和连接表

@Table({ tableName: "workout_tag", timestamps: false })
export class WorkoutTag extends Model<WorkoutTag> {
    @ForeignKey(() => Workout)
    @Column
    workoutId!: number;

    @ForeignKey(() => Tag)
    @Column
    tagId!: number;

    @BelongsTo(() => Workout)
    workout!: Workout;

    @BelongsTo(() => Tag)
    tag!: Tag;
}

我尝试过的查询:

        const include: Includeable[] = [{ model: User, as: "user" }];
        const additionalOptions: FindOptions<Partial<Workout>> = {};

        if ((filter?.tags?.length ?? 0) > 0) {
            console.log("Applying filter", filter?.tags);
            include.push({
                model: Tag,
                through: {
                    where: {
                        tagId: { [Op.in]: filter?.tags },
                    },
                },
            });
            additionalOptions.group = ["`Workout`.`id`"];
            additionalOptions.having = sequelize.literal(
                `COUNT(DISTINCT "\`tags->WorkoutTag\`.\`tagId\`") = ${filter!.tags!.length}`
            );
        }

        console.log("Loading workouts");

        const workouts = await Workout.findAll({
            include,
            limit: WORKOUTS_PER_PAGE,
            offset: (page - 1) * WORKOUTS_PER_PAGE,
            where: {
                name: { [Op.like]: `%${search}%` },
                length: { [Op.between]: [filter?.length?.min ?? 0, filter?.length?.max ?? 180] },
            },
            ...additionalOptions,
        });

我正在尝试获取与数组中指定的所有标签相关的所有锻炼。filter.tags

filter.tags是标记实体的 ID 数组

我使用的数据库是MySQL

这些是我使用的表格:

CREATE TABLE `workouts` (
   `id` int NOT NULL AUTO_INCREMENT,
   `name` varchar(255) DEFAULT NULL,
   `length` int DEFAULT NULL,
   `userId` int DEFAULT NULL,
   `createdAt` datetime DEFAULT NULL,
   `updatedAt` datetime DEFAULT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `unique-index` (`name`,`userId`),
   KEY `userId` (`userId`),
   CONSTRAINT `workouts_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
 ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci



CREATE TABLE `users` (
   `id` int NOT NULL AUTO_INCREMENT,
   `name` varchar(255) DEFAULT NULL,
   `createdAt` datetime DEFAULT NULL,
   `updatedAt` datetime DEFAULT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `name` (`name`),
   UNIQUE KEY `email` (`email`)
 ) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci



CREATE TABLE `tags` (
   `id` int NOT NULL AUTO_INCREMENT,
   `name` varchar(255) DEFAULT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `name` (`name`)
 ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci


CREATE TABLE `workout_tag` (
   `workoutId` int NOT NULL,
   `tagId` int NOT NULL,
   PRIMARY KEY (`workoutId`,`tagId`),
   UNIQUE KEY `workout_tag_tagId_workoutId_unique` (`workoutId`,`tagId`),
   KEY `tagId` (`tagId`),
   CONSTRAINT `workout_tag_ibfk_1` FOREIGN KEY (`workoutId`) REFERENCES `workouts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
   CONSTRAINT `workout_tag_ibfk_2` FOREIGN KEY (`tagId`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
SQL MySQL .js 续集 打字稿

评论

1赞 jQueeny 11/15/2023
您能否共享每个模型的架构,以便更轻松地提供解决方案?
0赞 JaRoMaster 11/16/2023
编辑了问题!
0赞 Rick James 11/18/2023
更进一步 - 请提供所涉及的每个MySQL表。我们可能必须在自定义 SQL 中执行任务。SHOW CREATE TABLE
0赞 JaRoMaster 11/18/2023
添加了 CREATE TABLE 的
0赞 Salman A 11/19/2023
是否可以接受纯 sql 解决方案(只需稍加努力即可转换为 Sequelize)?
1赞 Senthil P Nathan 11/21/2023
@JaRoMaster dbfiddle.uk/5JgIq-u1

答:

1赞 Salman A 11/20/2023 #1

您需要执行“关系划分”(推荐阅读)。

查找具有标签 1、3、4(可能更多)的所有锻炼的 SQL 解决方案是:

SELECT *
FROM workouts
WHERE NOT EXISTS (
    SELECT 1
    FROM (VALUES ROW(1), ROW(3), ROW(4)) AS required_tags(id)
    WHERE NOT EXISTS (
        SELECT 1
        FROM workout_tag
        WHERE workout_tag.workoutId = workouts.id AND workout_tag.tagId = required_tags.id
    )
);

或者,如果您发现条件聚合更容易理解,那么:

SELECT *
FROM workouts
WHERE id IN (
    SELECT workoutId
    FROM workout_tag
    WHERE tagId IN (1, 3, 4)
    GROUP BY workoutId
    HAVING COUNT(*) = 3
    -- the "= 3" part must match the number of items in the IN clause
)

DB<>小提琴

第二个查询更容易转换为 Sequelize,您可以将子句添加为 .havingSequelize.literal