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

TypeGoose创建文档时子文档转为ObjectId问题求助

TypeGoose子文档仅存储ObjectId问题排查与解决

问题现象

使用TypeGoose创建Game文档时,传入的scenes包含完整子文档数据(含sceneName、dmNotes、characters等字段),但数据库插入日志显示scenes仅被存储为ObjectId数组,而非完整嵌入的子文档:

测试数据代码:

const testData= {
  "title": "Test Game 1",
  "scenes": [
    {
      "sceneName": "Northern Milestone",
      "dmNotes": [
        {
          "content": "A crossroads where an path branches off towards a town with hostels. Much of the game's map can be seen with limited detail from the great plateau this crossroads sits atop."
        }
      ],
      "characters": [
        {
          "characterName": "Ian",
          "isPlayerCharacter": true,
        },
        {
          "characterName": "Wanderful"
        }
      ]
    },
    {
      "sceneName": "North Woods",
      "characters": [],
      "dmNotes": [
        {
          "content": "A thick woods, perceptive characters may smell faint smoke."
        }
      ]
    }
  ],
  "items": []
}

mongoose.set("debug", true);
await GameModel.create(testData);

数据库插入日志:

Mongoose: games.insertOne({ title: 'Test Game 1', templateChildren: [], scenes: [ { _id: ObjectId("64efc2a0eb4110d73787c7ff") }, { _id: ObjectId("64efc2a0eb4110d73787c800") } ], items: [], _id: ObjectId("64efc2a0eb4110d73787c7fe"), __v: 0}, {})

相关Schema定义

export class Character {
    @prop({required: true, default: false})
    isPlayerCharacter!: boolean;

    @prop({required: true, default: 'Unnamed Character'})
    characterName!: string;

    @prop({required: true, default: false})
    template!: boolean;

    @prop({type: () => [DMNote], default: []})
    dmNotes!: DMNote[];

    @prop({required: true, default: new Stats()})
    stats!: Stats;

    @prop({required: true, default: new SkillStats()})
    skills!: SkillStats;

    @prop({type: () => [Item], default: []})
    items!: Item[];

    @prop({required: true, default: 0})
    sceneId!: number;

    @prop({required: true, default: 0})
    skillStatsId!: number;

    @prop({ref: () => User, required: false})
    player?: Ref<User>;
}

export class SceneClass {
    @prop({required: true, default: 'Unnamed Scene'})
    sceneName!: string;

    @prop({required: true, default: false})
    template!: boolean;

    @prop({type: () => [Character], default: []})
    characters!: Character[];

    @prop({type: () => [DMNote], default: []})
    dmNotes!: DMNote[];

    @prop({type: () => [Item], default: []})
    items!: Item[];
}

export class Game {
    @prop()
    title?: string;

    @prop()
    isTemplate!: boolean;

    @prop({ref: () => Game})
    templateChildren?: Ref<Game>[];

    @prop({ref: () => Game})
    templateParent?: Ref<Game>;

    @prop({type: () => [SceneClass], required: true, default: []})
    scenes!: SceneClass[];

    @prop({type: () => [Item]})
    items: Item[] = [];

    @prop({ref: () => User})
    owner?: Ref<User>;

    public getCharacters(this: DocumentType<Game>) {
        return this.scenes.map((scene) => scene.characters || []).flat();
    }

    public findCharacterByName(this: DocumentType<Game>, characterName: string) {
        return this.getCharacters().find((character) => character.characterName === characterName);
    }

    public async getUsersCharacterInThisGame(this: DocumentType<Game>, user: DocumentType<User>) {
        return this.getCharacters().find((character) => character.player?._id.toHexString() === user._id.toHexString());
    }

    public async getSceneWithUser(this: DocumentType<Game>, user: DocumentType<User>) {
        const playedCharacter = await this.getUsersCharacterInThisGame(user);
        if (!playedCharacter) return undefined;
        return this.scenes.find((scene) => scene.characters.find((character) => character.characterName === playedCharacter.characterName))
    }
}

export class User {
    @prop({required: true, default: '', unique: true})
    userName!: String;

    @prop({type: () => [MessageClass], required: true, default: []})
    sentMessages?: MessageClass[];

    @prop({type: () => [MessageClass], required: true, default: []})
    receivedMessages!: MessageClass[];
}

问题原因

核心原因是SceneClass被意外注册为独立的Mongoose模型。当一个类被getModelForClass注册为模型后,TypeGoose/Mongoose会默认将该类型的数组字段视为引用关系,只存储ObjectId而非嵌入完整子文档。

解决方案

  1. 删除SceneClass的独立模型注册:检查代码中是否存在getModelForClass(SceneClass)这类注册语句,将其删除。SceneClass仅需作为Game类的子文档类型存在,无需单独注册模型。
  2. 显式强制嵌入模式:若确实需要SceneClass同时作为独立模型和嵌入子文档,可在Game类的scenes字段配置中添加ref: false,明确指定为嵌入模式:
@prop({ type: () => [SceneClass], required: true, default: [], ref: false })
scenes!: SceneClass[];
  1. 验证字段兼容性:确保测试数据字段与Schema定义匹配,比如SceneClass的template字段有默认值,测试数据未传该字段不影响存储,但必填字段缺失可能引发其他问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 07:17:04