How do I handle migrations that involve schema creation, data insertion, schema alteration over time?

Viewed 176

Below are my original entities:

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column()
  age: number;
}
@Entity()
export class Company {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;
}

Below is the migration script that I generated using typeorm migration:generate:

export class schemaCreation1627513975044 implements MigrationInterface {
  name = "schemaCreation1627513975044";

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `CREATE TABLE "company" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_056f7854a7afdba7cbd6d45fc20" PRIMARY KEY ("id"))`
    );
    await queryRunner.query(
      `CREATE TABLE "user" ("id" SERIAL NOT NULL, "firstName" character varying NOT NULL, "lastName" character varying NOT NULL, "age" integer NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`
    );

    const user = new User();
    user.age = 1;
    user.firstName = "first";
    user.lastName = "user";

    await queryRunner.manager.save(user);

    const company = new Company();
    company.name = "company";

    await queryRunner.manager.save(company);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "user"`);
    await queryRunner.query(`DROP TABLE "company"`);
  }
}

I run the script, the schema is created and is initialized with data. Later during the development of my app, I updated the User entity such that it now has a FK to Company and so the entities have been updated as follows:

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column()
  age: number;

  // Adding FK to Company
  @ManyToOne(() => Company, (company) => company.users)
  @JoinColumn({ name: "company_id" })
  company: Company;

  @Column({
    name: "company_id",
  })
  companyId: number;
}
@Entity()
export class Company {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToMany(() => User, (user) => user.company)
  users: User[];
}

And below is the migration script I created, using typeorm migration:create to update the existing users:

export class addCompanyToUser1627514375000 implements MigrationInterface {
  name = "addCompanyToUser1627514375000";

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "user" ADD "company_id" integer`);
    await queryRunner.query(
      `ALTER TABLE "user" ADD CONSTRAINT "FK_9e70b5f9d7095018e86970c7874" FOREIGN KEY ("company_id") REFERENCES "company"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
    );

    const targetCompany = await queryRunner.manager
      .createQueryBuilder(Company, "c")
      .where("c.name = :name", { name: "company" })
      .getOne();

    await queryRunner.manager
      .createQueryBuilder(User, "u")
      .update({
        companyId: targetCompany.id,
      })
      .execute();

    await queryRunner.query(
      'ALTER TABLE "user" ALTER COLUMN "company_id" SET NOT NULL'
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `ALTER TABLE "user" DROP CONSTRAINT "FK_9e70b5f9d7095018e86970c7874"`
    );
    await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "company_id"`);
  }
}

I run the new migration script, and the schema for user has been updated to include a company_id column. All of this work fine if the first script was run independently of the second script.

The Error

If, on the other hand, I run the scripts all at once, as in the case where I'm trying to stand up a new environment, I get the following error:

query failed: INSERT INTO "user"("firstName", "lastName", "age", "company_id") VALUES ($1, $2, $3, DEFAULT) RETURNING "id" -- PARAMETERS: ["first","user",1]
error: error: column "company_id" of relation "user" does not exist
    at Parser.parseErrorMessage (/Users/junior/Source/typeorm-migration-issue/node_modules/pg-protocol/dist/parser.js:287:98)
    at Parser.handlePacket (/Users/junior/Source/typeorm-migration-issue/node_modules/pg-protocol/dist/parser.js:126:29)
    at Parser.parse (/Users/junior/Source/typeorm-migration-issue/node_modules/pg-protocol/dist/parser.js:39:38)
    at Socket.<anonymous> (/Users/junior/Source/typeorm-migration-issue/node_modules/pg-protocol/dist/index.js:11:42)
    at Socket.emit (events.js:200:13)
    at addChunk (_stream_readable.js:290:12)
    at readableAddChunk (_stream_readable.js:271:11)
    at Socket.Readable.push (_stream_readable.js:226:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:166:17) {
  length: 127,
  name: 'error',
  severity: 'ERROR',
  code: '42703',
  detail: undefined,
  hint: undefined,
  position: '52',
  internalPosition: undefined,
  internalQuery: undefined,
  where: undefined,
  schema: undefined,
  table: undefined,
  column: undefined,
  dataType: undefined,
  constraint: undefined,
  file: 'parse_target.c',
  line: '1034',
  routine: 'checkInsertTargets'
}

I understand the error, but I don't know what the proper solution is. The core problem is the original shape of the User entity and corresponding schema that is generated, in schemaCreation1627513975044, does not match the current shape of User

I'm facing this issue in a much larger project where the schema has evolved over time and migration scripts were created that insert data based on the shape of the original Entity.

What would be the best way to handle this scenario?

Repo

See typeorm-migration-issue for working example.

  • yarn install
  • yarn build
  • yarn typeorm migration:run
0 Answers
Related