【问题标题】:Updating PSQL Tables with Relationships between Entities Using NestJS, TypeORM, GraphQL使用 NestJS、TypeORM、GraphQL 更新具有实体之间关系的 PSQL 表
【发布时间】: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 表之间的关系。这是我在实体中唯一更改的内容,我认为可能是我无法连接到这个新分支上的先前数据库的原因。如果您想查看我的服务、解析器或模块,请询问。我不相信它们会导致任何问题,因为如果我连接到新数据库,一切都会按预期编译和工作,不会引发任何错误。我真的只是在寻找有关如何调试此问题的任何见解。

【问题讨论】:

    标签: postgresql graphql nestjs typeorm


    【解决方案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,以便可以更改其他三个表中的关系。我使用的遗留代码做得很差,所以关系从一开始就是错误的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-06-05
      • 1970-01-01
      • 2021-07-16
      • 2021-04-11
      • 1970-01-01
      • 2018-09-10
      • 2022-01-22
      • 1970-01-01
      相关资源
      最近更新 更多