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

本地正常的代码部署到Vercel后出现TypeORM循环依赖错误

本地正常但Vercel部署后TypeORM循环依赖错误解决

问题背景

代码用于创建关联Exhibit与Painting的ExhibitPaintingDetail中间表记录,本地运行无问题,但部署到Vercel后触发TypeORM循环依赖错误,错误信息显示Cyclic dependency: "y"。

相关代码

API处理函数

import { AppDataSource } from "../../data-source";
import { Painting } from "../../entity/Painting";
import { Exhibit } from "../../entity/Exhibit";
import { ExhibitPaintingDetail } from "../../entity/ExhibitPaintingDetail";
import { NextApiRequest, NextApiResponse } from "next";
import test from "../../app/test/page";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "POST") {
    const paintingid =
      typeof req.query["paintingid"] === "string"
        ? Number.parseInt(req.query["paintingid"])
        : undefined;
    const exhibitid =
      typeof req.query["exhibitid"] === "string"
        ? Number.parseInt(req.query["exhibitid"])
        : undefined;

    try {
      if (!AppDataSource.isInitialized) {
        await AppDataSource.initialize();
      }
    } catch (e) {
      console.log(e);
    }

    const exhibitPaintingDetailRepository = await AppDataSource.getRepository(
      ExhibitPaintingDetail
    );

    if (paintingid !== undefined && exhibitid !== undefined) {
      const exhibitPaintingDetail = new ExhibitPaintingDetail();
      exhibitPaintingDetail.exhibit = { id: exhibitid } as Exhibit;
      exhibitPaintingDetail.painting = { id: paintingid } as Painting;

      try {
        const result = await exhibitPaintingDetailRepository.save(
          exhibitPaintingDetail
        );
        console.log("result: ", result);
        res
          .status(200)
          .json({ success: true, redirectUrl: `/painting?id=${paintingid}` });
      } catch (e) {
        return res.status(400).send({ success: false, error: e });
      }
    }
  } else {
    res.status(405).send({ error: "Method Not Allowed" });
  }
}

实体定义

ExhibitPaintingDetail

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  JoinColumn,
  ManyToOne,
} from "typeorm";
import { Exhibit } from "./Exhibit";
import { Painting } from "./Painting";

@Entity("exhibitpaintingdetail")
export class ExhibitPaintingDetail {
  @PrimaryGeneratedColumn()
  id?: number;

  @Column({ nullable: true })
  submissiondate: Date;

  @Column({ nullable: true })
  exhibitpaintingprice: number;

  @Column({ nullable: true })
  status: string;

  @Column({ nullable: true })
  outcome: string;

  @Column({ nullable: true })
  note: string;

  @ManyToOne(() => Exhibit, (exhibit) => exhibit.exhibitPaintingDetails, { cascade: false })
  @JoinColumn({ name: "exhibitid" })
  exhibit: Exhibit;

  @ManyToOne(() => Painting, (painting) => painting.exhibitPaintingDetails, { cascade: false })
  @JoinColumn({ name: "paintingid" })
  painting: Painting;
}

Exhibit

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToOne,
  OneToMany,
  JoinColumn,
} from "typeorm";
import { Gallery } from "./Gallery";
import { ExhibitPaintingDetail } from "./ExhibitPaintingDetail";

@Entity("exhibit")
export class Exhibit {
  @PrimaryGeneratedColumn()
  id?: number;

  @Column()
  name: string;

  @Column({ nullable: true })
  description: string;

  @Column({ default: true })
  isEnabled?: boolean;

  @Column({ nullable: true })
  startDate: Date;

  @Column({ nullable: true })
  endDate: Date;

  @Column({ nullable: true })
  dropOffDate: Date;

  @Column({ nullable: true })
  pickUpDate: Date;

  @Column({ nullable: true })
  galleryId: number;

  @Column({ nullable: true })
  note: string;

  @Column({ nullable: true })
  decisionDate?: Date;

  @Column({ nullable: true })
  website: string;

  @ManyToOne(() => Gallery, (gallery) => gallery.exhibits)
  @JoinColumn({ name: "galleryId" })
  gallery: Gallery;

  @OneToMany(
    () => ExhibitPaintingDetail,
    (exhibitPaintingDetail) => exhibitPaintingDetail.exhibit
  )
  exhibitPaintingDetails: ExhibitPaintingDetail[];
}

Painting

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToMany,
  JoinTable,
  OneToMany,
} from "typeorm";
import { ExhibitPaintingDetail } from "./ExhibitPaintingDetail";
import { Tag } from "./Tag";

@Entity("painting")
export class Painting {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  picture: string;

  @Column()
  description: string;

  @Column("int")
  artistId: number;

  @Column("int")
  month: number;

  @Column()
  year: number;

  @Column()
  media: string;

  @Column()
  price: number;

  @Column()
  width: number;

  @Column()
  height: number;

  @Column()
  sold: number;

  @Column({ nullable: true })
  depth: number;

  @Column({ nullable: true })
  note: string;

  @Column({ nullable: true })
  api_id: number;

  @Column({ nullable: true })
  permalink: string;

  @Column("timestamp without time zone", { nullable: true })
  timestamp: Date;

  @OneToMany(
    () => ExhibitPaintingDetail,
    (exhibitPaintingDetail) => exhibitPaintingDetail.painting
  )
  exhibitPaintingDetails: ExhibitPaintingDetail[];

  @ManyToMany(() => Tag, (tag) => tag.paintings)
  @JoinTable({
    name: "tagpaintings",
    joinColumn: {
      name: "paintingid",
      referencedColumnName: "id",
    },
    inverseJoinColumn: {
      name: "tagid",
      referencedColumnName: "id",
    },
  })
  tags: Tag[];
}

Vercel返回的错误信息

query: START TRANSACTION
query: ROLLBACK
TypeORMError: Cyclic dependency: "y"
    at visit (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:147:23)
    at visit (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:164:21)
    at SubjectTopoligicalSorter.toposort (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:143:17)
    at SubjectTopoligicalSorter.sort (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:53:45)
    at SubjectExecutor.execute (/var/task/node_modules/typeorm/persistence/SubjectExecutor.js:91:108)
    at EntityPersistExecutor.execute (/var/task/node_modules/typeorm/persistence/EntityPersistExecutor.js:140:36)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async l (/var/task/.next/server/pages/api/addpaintingtoexhibitw2params.js:1:12275)
 ⨯ TypeORMError: Cyclic dependency: "y"
    at visit (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:147:23)
    at visit (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:164:21)
    at SubjectTopoligicalSorter.toposort (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:143:17)
    at SubjectTopoligicalSorter.sort (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:53:45)
    at SubjectExecutor.execute (/var/task/node_modules/typeorm/persistence/SubjectExecutor.js:91:108)
    at EntityPersistExecutor.execute (/var/task/node_modules/typeorm/persistence/EntityPersistExecutor.js:140:36)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async l (/var/task/.next/server/pages/api/addpaintingtoexhibitw2params.js:1:12275)
TypeORMError: Cyclic dependency: "y"
    at visit (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:147:23)
    at visit (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:164:21)
    at SubjectTopoligicalSorter.toposort (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:143:17)
    at SubjectTopoligicalSorter.sort (/var/task/node_modules/typeorm/persistence/SubjectTopoligicalSorter.js:53:45)
    at SubjectExecutor.execute (/var/task/node_modules/typeorm/persistence/SubjectExecutor.js:91:108)
    at EntityPersistExecutor.execute (/var/task/node_modules/typeorm/persistence/EntityPersistExecutor.js:140:36)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async l (/var/task/.next/server/pages/api/addpaintingtoexhibitw2params.js:1:12275)
Error: Runtime exited with error: exit status 1
Runtime.ExitError

解决方案

1. 直接设置外键ID,避免加载完整实体

原代码查询完整的Painting和Exhibit实体并赋值给中间表,TypeORM默认会尝试级联保存关联实体,引发循环依赖。修改为仅传递ID对象,无需加载整个实体:

// 替换原有的查询实体代码
const exhibitPaintingDetail = new ExhibitPaintingDetail();
exhibitPaintingDetail.exhibit = { id: exhibitid } as Exhibit;
exhibitPaintingDetail.painting = { id: paintingid } as Painting;

2. 删除冗余字段

ExhibitPaintingDetail中的exhibitpaintingid字段为冗余字段,已有自增id作为主键,该字段会导致实体映射混淆,直接删除即可。

3. 关闭级联操作

在@ManyToOne装饰器中明确设置cascade: false,禁止TypeORM自动级联保存关联实体:

@ManyToOne(() => Exhibit, (exhibit) => exhibit.exhibitPaintingDetails, { cascade: false })
@JoinColumn({ name: "exhibitid" })
exhibit: Exhibit;

@ManyToOne(() => Painting, (painting) => painting.exhibitPaintingDetails, { cascade: false })
@JoinColumn({ name: "paintingid" })
painting: Painting;

4. 优化数据源初始化逻辑

确保数据源仅初始化一次,避免在Vercel无服务器环境中重复初始化导致的异常:

try {
  if (!AppDataSource.isInitialized) {
    await AppDataSource.initialize();
  }
} catch (e) {
  console.error("数据源初始化失败:", e);
  return res.status(500).send({ success: false, error: "数据库连接失败" });
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 15:17:06