You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

sequelize-typescript一对多关联查询仅返回单条数据问题排查

一对多关联查询仅返回单条关联数据的排查与解决

问题场景

使用sequelize-typescript定义Album与Photo一对多模型(一个Album对应多个Photo),通过findByPk查询Album并关联Photo时,数据库中明明存在3条以上关联Photo,但返回结果仅包含1条。

模型定义

Album.ts

import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from "sequelize-typescript";
import { Photo } from "./photo";

@Table({
  timestamps: true,
  deletedAt: "albumDeletedAt",
  paranoid: true,
  createdAt: true,
  updatedAt: true
})
export class Album extends Model {
  @PrimaryKey
  @Column({
    type: DataType.INTEGER,
    primaryKey: true,
    autoIncrement: true
  })
  declare id: number;

  @Column({
    type: DataType.STRING,
    allowNull: false
  })
  declare name: string;

  @Column({
    type: DataType.STRING,
    allowNull: true
  })
  declare description?: string;

  @Column({
    type: DataType.STRING,
    allowNull: false
  })
  declare thumbnailURL: string;

  @HasMany(() => Photo)
  declare photos: Photo[];

  @CreatedAt
  declare createdAt: Date;

  @UpdatedAt
  declare updatedAt: Date;
}

Photo.ts

import { BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from "sequelize-typescript";
import { Album } from "./album";

@Table({
  timestamps: true,
  deletedAt: "photoDeletedAt",
  paranoid: true,
  createdAt: true,
  updatedAt: true
})
export class Photo extends Model {
  @PrimaryKey
  @Column({
    type: DataType.INTEGER,
    primaryKey: true,
    autoIncrement: true
  })
  declare id: number;

  @Column({
    type: DataType.STRING,
    allowNull: false
  })
  declare name: string;

  @Column({
    type: DataType.STRING,
    allowNull: false
  })
  declare photoURL: string;

  @ForeignKey(() => Album)
  @Column({
    type: DataType.INTEGER,
    allowNull: true,
  })
  declare albumId: number;

  @BelongsTo(() => Album)
  declare album: Album;

  @CreatedAt
  declare createdAt: Date;

  @UpdatedAt
  declare updatedAt: Date;
}

原查询代码

public async getAlbumById(id: number): Promise<Album | null> {
    try {
        const searchedResult = await this.albumRepository.findByPk(id, {
            include: {
                model: this.photoRepository,
                where: {
                    albumId: id
                }
            },
            raw: true,
            nest: true
        });

        return searchedResult;
    } catch (error) {
        console.error(`Error finding the album via ID for the ID of ${id} due to`, error);
        return null;
    }
}

异常返回结果

{
"name": "Sample Card Photos Album (Test)",
"description": "Pictures for testing placeholder",
"thumbnailURL": "",
"photos": [
    {
        "name": "beautiful-village-snow-covered-mountain.jpg",
        "photoURL": "http://localhost:8080/uploads/pictures/Sample_Card_Photos_Album_(Test)/beautiful-village-snow-covered-mountain.jpg"
    }
],
"albumDeletedAt": null
}

问题原因排查

  1. raw: true的核心影响:启用raw: true后,Sequelize直接返回数据库查询的原始行数据,而非封装后的Model实例。一对多关联的SQL查询本质是JOIN操作,会生成多行结果(每行对应一个Photo+Album组合),但raw: true结合nest: true无法自动将多行关联数据聚合为数组,只会保留单条关联数据。
  2. 冗余的where条件:include中添加的where: { albumId: id }属于多余配置,findByPk已指定Album的主键,关联查询会自动通过外键匹配,该条件不会导致数据丢失,但无保留必要。

解决方法

方法1:移除raw: true,使用Model实例转换为纯JSON

优先推荐此方案,sequelize-typescript的Model实例会自动处理关联数据的聚合,确保返回完整的photos数组。若需纯JSON对象,可通过get({ plain: true })转换:

public async getAlbumById(id: number): Promise<Album | null> {
    try {
        const album = await this.albumRepository.findByPk(id, {
            include: { model: this.photoRepository }
        });

        // 转换为纯JSON对象(可选,若需返回Model实例则直接返回)
        return album ? album.get({ plain: true }) : null;
    } catch (error) {
        console.error(`Error finding the album via ID ${id}:`, error);
        return null;
    }
}

方法2:保留raw: true时添加分组配置

若必须使用raw模式,需通过group指定按Album主键分组,同时确保关联字段被正确聚合(需数据库支持聚合函数):

public async getAlbumById(id: number): Promise<Album | null> {
    try {
        const searchedResult = await this.albumRepository.findByPk(id, {
            include: {
                model: this.photoRepository,
                attributes: ['id', 'name', 'photoURL']
            },
            raw: true,
            nest: true,
            group: ['Album.id', 'Photos.id'] // 按Album和Photo的主键分组
        });

        return searchedResult;
    } catch (error) {
        console.error(`Error finding the album via ID ${id}:`, error);
        return null;
    }
}

额外检查点

  • 确认关联的Photo未被软删除:由于模型启用了paranoid: true,需检查Photo的photoDeletedAt字段是否为null,被软删除的Photo不会出现在查询结果中。
  • 验证数据库中外键关联:确认Photo表的albumId字段确实指向目标Album的id,无数据错误。

内容的提问来源于stack exchange,提问作者Adrian Joseph

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 12:24:58