本地正常的代码部署到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
相关产品推荐
相关产品推荐

