当前位置: 首页 > 知识库问答 >
问题:

使用NestJS、TypeORM、GraphQL更新实体间关系的PSQL表

谢飞舟
2023-03-14

我已经为创建新表和更新后端的TypeORM实体苦苦挣扎了一周。我们将NestJS、GraphQL和TypeORM与PSQL数据库一起使用。我们有一个生产服务器/数据库设置,客户信息已经保存。我正在尝试使用代码优先的方法向数据库中添加一个新表来生成模式。在repo的主分支上,我在本地环境中启动它,并连接到一个干净的数据库。创建帐户并将信息保存到表后,我将切换到一个新分支,该分支包含用于实现新表的代码,包括模块、服务、实体和解析器。如果我尝试运行这个分支并连接到我在master上使用的同一个数据库,它将无法编译,无法生成模式。gql文件,并在“GraphQLModule dependencies initialized”处停止我创建的这个新表与Teams表具有多通关系,其中已经包含了值。出于某种原因,我认为TypeORM无法正确更新数据库,我不知道为什么。如果我创建了一个新数据库,并使用新表代码连接到分支上的新数据库,它工作正常,不会抛出错误。问题是,如果我连接到原始数据库,不会抛出错误,但代码无法编译,我不知道如何调试它。

是否有人在使用TypeORM、Nest和GraphQL向其PSQL数据库添加新表时遇到任何问题?

下面是一些代码片段,说明了我的意思:

豁免表的实体(旧数据库中已存在)

@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;
}

这里是新的实体豁免模板,它与teams表有多通关系,并且存在于新的分支上

@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;
}

最后,这是团队表,它也存在于旧分支上。这是来自新分支的代码,该分支包含与WaiverTemplateEntity的新OneToMore关系。

@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表和faverertemplates表之间的关系。这是我在实体中更改的唯一内容,我认为这可能是我无法连接到这个新分支上以前的数据库的原因。如果您想查看我的服务、解析器或模块,请询问。我不认为它们会导致任何问题,因为如果我连接到一个新的数据库,一切都会按照预期进行编译和工作,不会抛出任何错误。我只是想了解一下如何调试这个问题。

共有1个答案

林雅畅
2023-03-14

如果有人对这个问题感兴趣,我今天终于解决了这个错误,至少在上表方面是这样。

使用TypeORM更改PSQL数据库时,最好使用typeorm迁移创建或生成自己的迁移文件:生成-n[迁移文件名称],然后使用typeorm迁移:run。生成命令将自动生成要运行的上下SQL迁移。您可以在此命令之前使用npx或从node_modules访问cli,因为只是运行typeorm命令给了我一个命令未找到错误。

然后,我查看了生成的迁移文件,发现我添加到表中的列没有设置为NULL,因此我发现前一个表中这些列的值为NULL的错误。我必须手动将NULL添加到这些列中的每一列,以便编译代码。但这很奇怪,因为我更新了实体,使这些字段的@Column decorators中有{nullable:true}

如果有人知道如何更好地使用TypeORM和Nest更改现有表中的关系,请联系我。我仍在为迁移文件手动编写SQL,以便可以更改其他三个表中的关系。我处理遗留代码做得很差,所以关系从一开始就是错误的。

 类似资料:
  • 我有一个实体像下面在Nestjs应用程序与typeorm为mongoDB: 我的其他实体扩展了审计,在我的应用程序中,我使用jwt来验证用户。 问题是,当我想保存一个实体,我不知道如何设置createdBy与@Before插入钩子... 我知道请求中有用户,但我不知道将用户引入方法的正确方法是什么?

  • 假设我有一个类型化实体定义,如下所示: 所以它有几个实体关系,OneToMany/onetomone 在制作DTO时,我该如何实现这一点? 这里我有一个例子DTO: 我不清楚通过Graphql的最佳方法 当前graphql接口: 如果我正在设计一个传统的RESTful服务,我会通过一个单独的endpoint在DB中创建文档,等待成功返回 然后在创建新的(在单独的终结点)时,将这些id指定为字段中的

  • 我有两个实体(表):用户和角色。两个实体之间有多对一的关系(一个用户可以有一个角色,角色可以与许多用户相关)。现在我想通过执行以下命令来更新用户实体(也可以包括用户角色的更新): (要更新的用户属性)与数据库中的用户数据合并,包括 (合并对象)保存到数据库 新的用户对象返回给调用者(包括角色) 用户已合并,但未加载(但是如果我向添加子属性,则没关系) (合并对象)保存到数据库(如果它是一部分,则也

  • 我试图使用项目外部定义的类型orm实体。也就是说,我有两个项目:模型和核心api。Models是导出实体定义的npm包。因此,从核心api(nestjs项目)开始,我尝试使用这些实体定义连接。大概是这样的: 然后,在一个模块中,我从npm包导入A实体: 这将导致下一个错误: 我完全不知道如何跟随,因为我不明白它在引擎盖下是如何工作的。我看到了typeorm和@nestjs/typeorm的代码,但

  • 文章实体 标签实体 然后我测试新增,随便添加几条数据 查询文章id=1 查询标签id=6 删除文章没问题 但是,删除标签id=6就报错了。服务器错误

  • 我整天都在尝试将实体保存到MySQL数据库中,遇到了巨大的困难。我正在使用NestJS和TypeORM。 教师实体ts app.module.ts 以下是错误: 该实体实际上保存在数据库中,但NestJS需要整整一分钟才能完成此任务并返回响应。我正在使用Angular,它等待这个响应,以便在注册为教师后将用户重定向到登录页面。在发送post请求在数据库中创建教师后,我仅在一分钟内收到状态为500的