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

NestJS+TypeORM更新含子实体列表的Chat实体报错求助

解决TypeORM更新Chat实体时的Error: Cannot query across one-to-many for property messages错误

我在更新包含新消息的Chat实体时触发报错:Error: Cannot query across one-to-many for property messages,尝试拼接现有消息与新消息后调用chatRepository.update方法,结果出现了这个问题。相关代码如下:

chat.entity.ts

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToOne,
  JoinColumn,
  OneToMany,
  OneToOne,
} from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { ChatMessage } from '../chatMessage/chatMessage.entity';
import { User } from 'src/auth/user.entity';
import { Session } from 'src/sessions/session.entity';

@Entity()
export class Chat {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => User, (user) => user.chats)
  @ApiProperty({
    type: () => User,
  })
  user: User;

  @ApiProperty({
    example: '2024-02-09T22:15:11.293Z',
    description: 'session_id',
  })
  @Column()
  session_id: string;

  @ApiProperty({
    type: () => Session,
    example: '',
    description: '',
  })
  @OneToOne(() => Session)
  @JoinColumn()
  session: Session;

  @ApiProperty({
    type: () => ChatMessage,
    example: '',
    description: 'chat message',
  })
  @OneToMany(() => ChatMessage, (chatMessage) => chatMessage.chat)
  @Column('jsonb', { default: [] })
  @JoinColumn()
  messages: ChatMessage[];
}

chatMessage.entity.ts

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  OneToMany,
  JoinColumn,
} from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { Chat } from '../chat/chat.entity';

export class DataDto {
  // Not sure why type is in both objects but that's how langchain structures it.
  @ApiProperty({ example: 'human', description: 'type' })
  @Column()
  type: string;

  @ApiProperty({
    example:
      'Mr. Smith is an investment banker at a large bank and primarily works with water risk data. What product or products should we recommend?',
    description: 'content',
  })
  @Column()
  content: string;

  @Column('jsonb', { nullable: true })
  additional_kwargs: any; // TODO: Figure out how to define this type

  @ApiProperty({ example: false, description: 'Bool for example' })
  @Column()
  example: boolean;
}

@Entity()
export class ChatMessage {
  @PrimaryGeneratedColumn()
  id: number;

  @ApiProperty({
    type: () => Chat,
    example: '',
    description: '',
  })
  @OneToMany(() => Chat, (chat: Chat) => chat.messages)
  @JoinColumn()
  chat: Chat;

  @ApiProperty({
    example: 'human',
    description: 'type',
    nullable: true,
    enum: ['human', 'bot'],
  })
  @Column({ nullable: true, enum: ['human', 'bot'] })
  type: string;

  @Column('jsonb', { nullable: true })
  data: DataDto;
}

chat.service.ts

@Injectable()
export class ChatService {
  constructor(
    @InjectRepository(Chat)
    private chatRepository: Repository<Chat>,
  ) {}
  async patch(id: number, Chat: Partial<Chat>): Promise<any> {
    const chat = await this.chatRepository.findOne({ where: { id } });
    if (!chat) {
      throw new Error('Chat not found');
    }
    console.debug('ChatService.patch chat:', chat);
    console.debug('ChatService.patch Chat:', Chat);
    const concatenatedMessages = chat.messages.concat(Chat.messages);
    console.debug(
      'ChatService.patch concatenatedMessages:',
      concatenatedMessages,
    );
    return await this.chatRepository.update(id, {
      messages: concatenatedMessages,
    });
}

问题根源

  1. 实体关系定义错误:
    • Chat实体的messages字段同时用了@OneToMany(关系型关联)和@Column('jsonb')(JSON列存储),两者冲突,TypeORM无法同时处理。
    • ChatMessage实体的chat字段用了@OneToMany,实际应该是@ManyToOne——一条Chat对应多条ChatMessage,反过来一条ChatMessage只属于一个Chat。
  2. 更新逻辑错误:
    • 查询Chat时未预加载messages关联,直接访问chat.messages会触发懒加载,但update方法无法直接处理关联对象数组的拼接。

修复方案

1. 修正实体关系

修改chat.entity.ts

去掉@Column('jsonb', { default: [] })和@JoinColumn()(@OneToMany不需要该装饰器,外键在ChatMessage侧),添加cascade: true让TypeORM自动处理子实体的保存:

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToOne,
  JoinColumn,
  OneToMany,
  OneToOne,
} from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { ChatMessage } from '../chatMessage/chatMessage.entity';
import { User } from 'src/auth/user.entity';
import { Session } from 'src/sessions/session.entity';

@Entity()
export class Chat {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => User, (user) => user.chats)
  @ApiProperty({
    type: () => User,
  })
  user: User;

  @ApiProperty({
    example: '2024-02-09T22:15:11.293Z',
    description: 'session_id',
  })
  @Column()
  session_id: string;

  @ApiProperty({
    type: () => Session,
    example: '',
    description: '',
  })
  @OneToOne(() => Session)
  @JoinColumn()
  session: Session;

  @ApiProperty({
    type: () => ChatMessage,
    example: '',
    description: 'chat message',
  })
  @OneToMany(() => ChatMessage, (chatMessage) => chatMessage.chat, { cascade: true })
  messages: ChatMessage[];
}
修改chatMessage.entity.ts

把@OneToMany改成@ManyToOne,修正关联方向:

import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  ManyToOne,
} from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { Chat } from '../chat/chat.entity';

export class DataDto {
  // Not sure why type is in both objects but that's how langchain structures it.
  @ApiProperty({ example: 'human', description: 'type' })
  @Column()
  type: string;

  @ApiProperty({
    example:
      'Mr. Smith is an investment banker at a large bank and primarily works with water risk data. What product or products should we recommend?',
    description: 'content',
  })
  @Column()
  content: string;

  @Column('jsonb', { nullable: true })
  additional_kwargs: any; // TODO: Figure out how to define this type

  @ApiProperty({ example: false, description: 'Bool for example' })
  @Column()
  example: boolean;
}

@Entity()
export class ChatMessage {
  @PrimaryGeneratedColumn()
  id: number;

  @ApiProperty({
    type: () => Chat,
    example: '',
    description: '',
  })
  @ManyToOne(() => Chat, (chat) => chat.messages)
  chat: Chat;

  @ApiProperty({
    example: 'human',
    description: 'type',
    nullable: true,
    enum: ['human', 'bot'],
  })
  @Column({ nullable: true, enum: ['human', 'bot'] })
  type: string;

  @Column('jsonb', { nullable: true })
  data: DataDto;
}

2. 调整ChatService的更新逻辑

使用TypeORM的关联操作,先预加载messages,再添加新消息并保存:

@Injectable()
export class ChatService {
  constructor(
    @InjectRepository(Chat)
    private chatRepository: Repository<Chat>,
  ) {}
  
  async patch(id: number, newChatData: Partial<Chat>): Promise<Chat> {
    // 预加载messages关联
    const chat = await this.chatRepository.findOne({ 
      where: { id },
      relations: ['messages'] 
    });
    
    if (!chat) {
      throw new Error('Chat not found');
    }
    
    // 处理传入的新消息
    if (newChatData.messages?.length) {
      // 为每条新消息绑定当前Chat
      newChatData.messages.forEach(msg => {
        msg.chat = chat;
      });
      // 合并消息并保存Chat,cascade会自动保存新的ChatMessage
      chat.messages.push(...newChatData.messages);
      return this.chatRepository.save(chat);
    }
    
    return chat;
  }
}

额外说明

  • 如果你更倾向于用JSON列存储消息而不是关系型关联,那直接去掉@OneToMany装饰器,只保留@Column('jsonb', { default: [] }),此时更新逻辑可以直接拼接数组后调用update,但这样会失去数据库层面的关联约束和查询灵活性。
  • 关联关系中,cascade: true会让TypeORM在保存Chat时自动保存关联的ChatMessage,无需手动调用ChatMessage的save方法。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 18:20:56