使用 NestJS、TypeORM、GraphQL 更新具有实体之间关系的 PSQL 表
Posted
技术标签:
【中文标题】使用 NestJS、TypeORM、GraphQL 更新具有实体之间关系的 PSQL 表【英文标题】:Updating PSQL Tables with Relationships between Entities Using NestJS, TypeORM, GraphQL 【发布时间】:2021-06-02 15:46:05 【问题描述】:我已经为创建新表和更新后端的 TypeORM 实体苦苦挣扎了一周。我们将 NestJS、GraphQL 和 TypeORM 与 PSQL 数据库一起使用。我们有一个生产服务器/数据库设置,其中已经保存了客户的信息。我正在尝试使用代码优先的方法向数据库添加一个新表来生成模式。在 repo 的 master 分支上,我在本地环境中启动它,并连接到一个干净的数据库。创建帐户并将信息保存到表后,我会切换到一个新分支,其中包含用于实现新表的代码,包括模块、服务、实体和解析器。如果我尝试运行此分支并连接到我在 master 上使用的同一个数据库,它将无法编译,无法生成 schema.gql 文件,并在“GraphQLModule 依赖项已初始化”处停止。我创建的这个新表与 Teams 表具有多对一关系,其中已经包含值。出于某种原因,我认为 TypeORM 无法正确更新数据库,我不知道为什么。如果我创建一个新数据库,并使用新表代码连接到分支上的新数据库,它就可以正常工作,并且不会引发任何错误。问题是我连接原来的数据库,没有报错,但是代码编译失败,不知道怎么调试。
是否有人在使用 TypeORM、Nest 和 GraphQL 将新表添加到他们的 PSQL 数据库时遇到任何问题?
这里有一些代码 sn-ps 说明了我的意思:
豁免表实体(已存在于旧数据库中)
@Entity( name: 'waivers' )
@ObjectType()
export class WaiverEntity extends BaseEntity
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field(() => AccountEntity)
@ManyToOne(
() => AccountEntity,
creator => creator.waivers,
onDelete: 'SET NULL' ,
)
@JoinColumn()
creator: Promise<AccountEntity>;
@Field(() => TeamEntity)
@ManyToOne(
() => TeamEntity,
team => team.waivers,
onDelete: 'CASCADE' ,
)
@JoinColumn()
team: Promise<TeamEntity>;
@Field(() => ID)
@Column( nullable: true )
creatorId: string;
@Field(() => ID)
@Index()
@Column( nullable: true )
teamId: string;
@Field()
@Column('json')
organizer: Organizer;
@Field()
@Column('json')
event: Event;
@Field()
@Column('json', nullable: true )
eventDate: EventDate;
@Field( nullable: true )
@Column()
includeEmergencyContact: boolean;
@Field( nullable: true )
@Column( nullable: true )
customerLabel: string;
@Field(() => CustomEntity, nullable: true, defaultValue: [] )
@Column('jsonb', nullable: true )
intensity: CustomEntity;
@Field(() => [CustomEntity], nullable: true, defaultValue: [] )
@Column('jsonb', nullable: true )
activities: CustomEntity[];
@Field( defaultValue: waiverStatus.DRAFT, nullable: false )
@Column( default: waiverStatus.DRAFT, nullable: false )
status: string;
@Field( nullable: true )
@Column( type: 'varchar', nullable: true )
title: string;
@Field( nullable: true )
@Column( nullable: true )
body: string;
@Field( nullable: true )
@Column( nullable: true, default: signatureDefaultContent )
signatureContent: string;
@Field(() => [String], nullable: true )
@Column('simple-array', nullable: true )
ageGroup: string[];
@Field(() => [AdditionalFields], nullable: false, defaultValue: [] )
@Column('jsonb', nullable: true )
additionalFields: AdditionalFields[];
@Field( nullable: false )
@Column( nullable: false )
step: number;
@Exclude()
@Field( nullable: true )
@Column( nullable: true, unique: true )
pdfURL: string;
@BeforeInsert()
cleanUpBeforeUpdate(): void
// add Prefix on retrieval
if (this.organizer && this.organizer.photoURL)
try
const photoUrls = this.organizer.photoURL.split(
`$AWS_BUCKETS.ORGANIZATION_BUCKET_IMAGE/`,
);
this.organizer.photoURL =
photoUrls.length > 1 ? photoUrls[1] : this.organizer.photoURL;
catch (e)
@AfterLoad()
updateURLs(): void
// add Prefix on retrieval
this.pdfURL = this.pdfURL
? `$getBucketPrefix(
AWS_BUCKETS_TYPES.WAIVER_BUCKET_FILES,
'https://',
)/$this.pdfURL`
: null;
if (this.organizer)
this.organizer.photoURL = this.organizer.photoURL
? `$getBucketPrefix(
AWS_BUCKETS_TYPES.ORGANIZATION_BUCKET_IMAGE,
'https://',
)/$this.organizer.photoURL`
: null;
@Field( nullable: true )
@Column( type: 'timestamp', nullable: true )
@IsDate()
publishDate: Date;
@Field( nullable: true )
@Column( nullable: true, unique: true )
slug: string;
@Field(() => [DownloadEntity], nullable: true )
@OneToMany(
() => DownloadEntity,
downloadEntity => downloadEntity.waiver,
)
@JoinColumn()
waiverDownloads: Promise<DownloadEntity[]>;
@Field( defaultValue: 0 )
downloadCount: number;
@Field(() => [WaiverMembersEntity])
@OneToMany(
() => WaiverMembersEntity,
waiverMember => waiverMember.account,
)
accountConnection: Promise<WaiverMembersEntity[]>;
@Field(() => [WaiverConsentsEntity])
@OneToMany(
() => WaiverConsentsEntity,
waiverMember => waiverMember.waiver,
)
consent: Promise<WaiverConsentsEntity[]>;
@Field(() => [AccountEntity])
waiverMember: AccountEntity[];
@Field(() => [ParticipantsEntity])
@OneToMany(
() => ParticipantsEntity,
participant => participant.waiver,
)
participants: ParticipantsEntity[];
@Field( defaultValue: 0 )
totalResponses: number;
@Field()
eventName: string;
@Field( nullable: true )
@Column( type: 'varchar', nullable: true )
smsContent: string;
@Field( nullable: true )
@Column( nullable: true )
smsCode: string;
@Field()
@Column( type: 'timestamp', default: () => timeStamp )
@IsDate()
createdAt: Date;
@Field()
@Column(
type: 'timestamp',
default: () => timeStamp,
onUpdate: timeStamp,
)
@IsDate()
lastUpdatedAt: Date;
这里是新的实体豁免模板,它与团队表具有多对一关系,并且存在于新分支上
@Entity( name: 'waiverTemplates' )
@ObjectType()
export class WaiverTemplateEntity extends BaseEntity
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field(() => TeamEntity)
@ManyToOne(
() => TeamEntity,
team => team.waiverTemplates,
onDelete: 'CASCADE', eager: true ,
)
@JoinColumn()
team: Promise<TeamEntity>;
@Field(() => ID)
@Index()
@Column( nullable: true )
teamId: string;
@Field()
@Column('json')
event: Event;
@Field()
@Column('json')
eventDate: EventDate;
@Field( nullable: true )
@Column( nullable: true )
includeEmergencyContact: boolean;
@Field( nullable: true )
@Column( nullable: true )
customerLabel: string;
@Field(() => CustomEntity, nullable: true, defaultValue: [] )
@Column('jsonb', nullable: true )
intensity: CustomEntity;
@Field(() => [CustomEntity], nullable: true, defaultValue: [] )
@Column('jsonb', nullable: true )
activities: CustomEntity[];
@Field( defaultValue: waiverStatus.DRAFT, nullable: false )
@Column( default: waiverStatus.DRAFT, nullable: false )
status: string;
@Field( nullable: true )
@Column( type: 'varchar', nullable: true )
title: string;
@Field( nullable: true )
@Column( nullable: true )
body: string;
@Field( nullable: true )
@Column( nullable: true, default: signatureDefaultContent )
signatureContent: string;
@Field(() => [String], nullable: true )
@Column('simple-array', nullable: true )
ageGroup: string[];
@Field(() => [AdditionalFields], nullable: false, defaultValue: [] )
@Column('jsonb', nullable: true )
additionalFields: AdditionalFields[];
@Field()
eventName: string;
最后,这里是teams 表,它也存在于旧分支上。这是来自新分支的代码,其中包含与 WaiverTemplateEntity 的新 OneToMany 关系。
@Entity( name: 'teams' )
@ObjectType()
export class TeamEntity extends BaseEntity
@Field(() => ID)
@PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@Column('varchar')
title: string;
@Field( nullable: true )
@Column('varchar', nullable: true )
taxID?: string;
@Field( nullable: true )
@Column(simpleJSON, nullable: true )
type: CustomEntity;
@Field( nullable: true )
@Column('varchar', nullable: true )
description?: string;
@Field(() => AccountEntity, nullable: false )
@OneToOne(
() => AccountEntity,
accountEntity => accountEntity.organization,
nullable: true, onDelete: 'SET NULL' ,
)
creator: AccountEntity;
@Field( nullable: true )
@Column( nullable: true )
creatorId: string;
@Field(() => BillingEntity, nullable: true )
@OneToOne(
() => BillingEntity,
billingEntity => billingEntity.team,
cascade: true ,
)
billingInformation: Promise<BillingEntity>;
@Field( nullable: true )
@Column('varchar', nullable: true )
photoURL?: string;
@Field( defaultValue: false )
@Column( default: false )
nonProfitFreemium: boolean;
@AfterLoad()
updateURLs(): void
// add Prefix on retrieval
this.photoURL = this.photoURL
? `$getBucketPrefix(
AWS_BUCKETS_TYPES.ORGANIZATION_BUCKET_IMAGE,
'https://',
)/$this.photoURL`
: null;
@Field(() => [CardEntity], nullable: true )
@OneToMany(
() => CardEntity,
cardEntity => cardEntity.holder,
cascade: true ,
)
cards: Promise<CardEntity[]>;
@Field( nullable: true, defaultValue: )
@Column(simpleJSON, nullable: true )
location?: LocationEntity;
@Field( nullable: true, defaultValue: )
@Column(simpleJSON, nullable: true )
contact?: ContactEntity;
@Field( nullable: true )
@Column( nullable: true )
numberOfEmployees?: string;
@Field( nullable: true )
@Column( nullable: true )
stripeId?: string;
@Field()
@Column( type: 'timestamp', default: () => 'CURRENT_TIMESTAMP(6)' )
@IsDate()
createdAt: Date;
@Field()
@Column(
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP(6)',
onUpdate: 'CURRENT_TIMESTAMP(6)',
)
@IsDate()
lastUpdatedAt: Date;
@Field(() => [InvitationEntity])
@OneToMany(
() => InvitationEntity,
invitationEntity => invitationEntity.team,
)
invitations: Promise<InvitationEntity[]>;
@Field(() => [WaiverEntity])
@OneToMany(
() => WaiverEntity,
waiver => waiver.team,
)
waivers: Promise<WaiverEntity[]>;
@Field( nullable: true )
@Column( default: () => 0 )
credits: number;
@Field( nullable: true )
@Column( default: () => false )
autoReload: boolean;
@Field( nullable: true )
@Column( default: () => 0 )
autoReloadAmount: number;
@Field( nullable: true )
@Column( default: () => 0 )
autoReloadMinAmount: number;
@Field( nullable: true )
@Column( type: 'float', default: 0.0 )
fixedWaiverPrice: number;
@Field(() => [TransactionEntity])
@OneToMany(
() => TransactionEntity,
transaction => transaction.team,
)
transactions: Promise<TransactionEntity[]>;
@Field(() => [WaiverTemplateEntity])
@OneToMany(
() => WaiverTemplateEntity,
waiverTemplate => waiverTemplate.team,
)
waiverTemplates: Promise<WaiverTemplateEntity[]>;
我知道表中有很多列,但需要注意的是 Teams 表和 WaiverTemplates 表之间的关系。这是我在实体中唯一更改的内容,我认为可能是我无法连接到这个新分支上的先前数据库的原因。如果您想查看我的服务、解析器或模块,请询问。我不相信它们会导致任何问题,因为如果我连接到新数据库,一切都会按预期编译和工作,不会引发任何错误。我真的只是在寻找有关如何调试此问题的任何见解。
【问题讨论】:
【参考方案1】:如果有人对此问题感兴趣,我今天终于解决了错误,至少在上面的表格方面。
使用 TypeORM 更改 PSQL 数据库时,最好使用 typeorm migration:generate -n [name of migration file]
创建或生成自己的迁移文件,然后
typeorm migration:run
。 generate 命令将自动生成一个向上和向下 SQL 迁移以运行。您可以在此命令之前使用npx
或从 node_modules 访问 cli,因为仅运行 typeorm 命令会给我一个 command not found 错误。
然后我查看了生成的迁移文件,你瞧,我添加到表中的列没有设置为NULL
,因此我在上一个表中的这些列的值有错误为空。我必须手动将NULL
添加到每个列中才能编译代码。不过这很奇怪,因为我将实体更新为在这些字段的 @Column 装饰器中包含 nullable: true
。
如果有人知道如何使用 TypeORM 和 Nest 更好地改变现有表中的关系,请与我联系。我仍在为迁移文件手动编写 SQL,以便可以更改其他三个表中的关系。我使用的遗留代码做得很差,所以关系从一开始就是错误的。
【讨论】:
以上是关于使用 NestJS、TypeORM、GraphQL 更新具有实体之间关系的 PSQL 表的主要内容,如果未能解决你的问题,请参考以下文章
将 TypeORM 实体模型类与 NestJS-GraphQL 模式类型结合使用好吗?
NestJS + Typeorm + Graphql:嵌套关系中 DTO 的正确设计模式