Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Behaviour of flush change in v5 when updating an entity property with a new entity #2781

Closed
co-sic opened this issue Feb 17, 2022 · 6 comments
Labels
bug Something isn't working

Comments

@co-sic
Copy link
Contributor

co-sic commented Feb 17, 2022

Describe the bug

@Entity({ abstract: true })
export abstract class BaseEntity extends MikroORMBaseEntity<BaseEntity, 'id'> {
  @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
  id!: string;

  @Property({ type: Date })
  @Index()
  createdAt: Date;

  @Property({ type: Date, onUpdate: () => new Date() })
  updatedAt: Date;

  protected constructor() {
    super();
    this.createdAt = new Date();
    this.updatedAt = new Date();
  }
}

@Entity()
export class Address extends BaseEntity {
  @Property()
  companyName: string;

  constructor(companyName: string) {
    super();
    this.companyName = companyName;
  }
}

@Entity()
@Index({ properties: ['id', 'createdAt'], name: 'customer_sorted_id' })
export class Customer extends BaseEntity {
  @Property()
  @Unique()
  @Index()
  customerNumber: string;

  @ManyToOne()
  companyAddress: Address;

  constructor(customerNumber: string, companyAddress: Address) {
    super();
    this.customerNumber = customerNumber;
    this.companyAddress = companyAddress;
  }
}

If i have a customer entity in the database already with an address, an then set a new Address entity, that is not already in the DB on the address property and call flush, i get the Error below. In v4 this was working.

Stack trace

NotNullConstraintViolationException: update "customer" set "company_address_id" = NULL, "updated_at" = '2022-02-17T15:26:07.667Z' where "id" = '3d21eae5-81f0-4f4c-97ce-fc7db43c1f95' - null value in column "company_address_id" of relation "customer" violates not-null constraint
    at PostgreSqlExceptionConverter.convertException (/home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/postgresql/PostgreSqlExceptionConverter.js:24:24)
    at PostgreSqlDriver.convertException (/home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/core/drivers/DatabaseDriver.js:180:54)
    at /home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/core/drivers/DatabaseDriver.js:184:24
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at PostgreSqlDriver.nativeUpdate (/home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/knex/AbstractSqlDriver.js:248:19)
    at ChangeSetPersister.persistManagedEntity (/home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/core/unit-of-work/ChangeSetPersister.js:137:21)
    at ChangeSetPersister.executeUpdates (/home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/core/unit-of-work/ChangeSetPersister.js:42:13)
    at ChangeSetPersister.runForEachSchema (/home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/core/unit-of-work/ChangeSetPersister.js:68:13)
    at UnitOfWork.commitUpdateChangeSets (/home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/core/unit-of-work/UnitOfWork.js:666:9)
    at UnitOfWork.persistToDatabase (/home/patrick/development/gastromatic/contract-service/node_modules/@mikro-orm/core/unit-of-work/UnitOfWork.js:597:13)

previous error: update "customer" set "company_address_id" = NULL, "updated_at" = '2022-02-17T15:26:07.667Z' where "id" = '3d21eae5-81f0-4f4c-97ce-fc7db43c1f95' - null value in column "company_address_id" of relation "customer" violates not-null constraint
    at Parser.parseErrorMessage (/home/patrick/development/gastromatic/contract-service/node_modules/pg-protocol/src/parser.ts:369:69)
    at Parser.handlePacket (/home/patrick/development/gastromatic/contract-service/node_modules/pg-protocol/src/parser.ts:188:21)
    at Parser.parse (/home/patrick/development/gastromatic/contract-service/node_modules/pg-protocol/src/parser.ts:103:30)
    at Socket.<anonymous> (/home/patrick/development/gastromatic/contract-service/node_modules/pg-protocol/src/index.ts:7:48)
    at Socket.emit (node:events:390:28)
    at addChunk (node:internal/streams/readable:315:12)
    at readableAddChunk (node:internal/streams/readable:289:9)
    at Socket.Readable.push (node:internal/streams/readable:228:10)
    at TCP.onStreamRead (node:internal/stream_base_commons:199:23)
    at TCP.callbackTrampoline (node:internal/async_hooks:130:17)

To Reproduce
Minimal example:

const address1 = new Address('test1');
    const customer = new Customer('100', address1);
    orm.entityManager.persist(customer);
    await orm.entityManager.flush();

    // this normally happens in a second request, but it behaves the same if i call it directly here without fetching the customer first
    const address2 = new Address('test2');
    customer.companyAddress = address2;
    await orm.entityManager.flush();

Versions

Dependency Version
node 16.13.1
typescript 4.5.5
mikro-orm 5.0.2
mikro-orm/postgresql 5.0.2
@B4nan
Copy link
Member

B4nan commented Feb 17, 2022

Sounds like you oversimplified the repro. Let me guess, you have also 1:m inverse side defined for that? Sounds like you are missing orphanRemoval on that.

@co-sic
Copy link
Contributor Author

co-sic commented Feb 17, 2022

I tested it with exactly the entities i posted above, nothing else. And why would i need orphanRemoval? I don't have a problem with removing the old entity, but updating the customer with the new address that i created.

This is my config:

const config: Options = {
  debug: false,
  entities: [Address, Customer],
  type: 'postgresql',
  clientUrl: process.env.DATABASE_URI,
  password: process.env.DATABASE_PASSWORD,
  metadataProvider: TsMorphMetadataProvider,
};

migration:

export class Migration20220217151832 extends Migration {

  async up(): Promise<void> {
    this.addSql('create table "address" ("id" uuid not null default gen_random_uuid(), "created_at" timestamptz(0) not null, "updated_at" timestamptz(0) not null, "company_name" varchar(255) not null);');
    this.addSql('create index "address_created_at_index" on "address" ("created_at");');
    this.addSql('alter table "address" add constraint "address_pkey" primary key ("id");');

    this.addSql('create table "customer" ("id" uuid not null default gen_random_uuid(), "created_at" timestamptz(0) not null, "updated_at" timestamptz(0) not null, "customer_number" varchar(255) not null, "company_address_id" uuid not null);');
    this.addSql('create index "customer_created_at_index" on "customer" ("created_at");');
    this.addSql('create index "customer_customer_number_index" on "customer" ("customer_number");');
    this.addSql('alter table "customer" add constraint "customer_customer_number_unique" unique ("customer_number");');
    this.addSql('create index "customer_sorted_id" on "customer" ("id", "created_at");');
    this.addSql('alter table "customer" add constraint "customer_pkey" primary key ("id");');

    this.addSql('alter table "customer" add constraint "customer_company_address_id_foreign" foreign key ("company_address_id") references "address" ("id") on update cascade;');
  }

  async down(): Promise<void> {
    this.addSql('alter table "customer" drop constraint "customer_company_address_id_foreign";');

    this.addSql('drop table if exists "address" cascade;');

    this.addSql('drop table if exists "customer" cascade;');
  }

}

Extending my address definition with reverse site and orphanRemoval like this also does not help:

@Entity()
export class Address extends BaseEntity {
  @Property()
  companyName: string;

  @OneToMany(() => Customer, (customer) => customer.companyAddress, { orphanRemoval: true })
  customers = new Collection<Customer>(this);

  constructor(companyName: string) {
    super();
    this.companyName = companyName;
  }
}

@co-sic
Copy link
Contributor Author

co-sic commented Feb 17, 2022

The fix i did for now is that i call flush after i created the new address and then flush again after i update the customer, this works. But i would rather call flush at the end of everything, so it's an all-or-nothing-transaction.

@B4nan
Copy link
Member

B4nan commented Feb 17, 2022

Interesting, the issue is with the database generated UUID. If you provide the value on runtime, it is passing. So it actually tries to set the relation before creating the address.

  @PrimaryKey({ type: 'uuid' })
  id: string = v4();

@B4nan B4nan added the bug Something isn't working label Feb 17, 2022
@B4nan
Copy link
Member

B4nan commented Feb 17, 2022

This is soo weird.

It is actually caused by the onUpdate callback on your updatedAt property.

@co-sic
Copy link
Contributor Author

co-sic commented Feb 17, 2022

Testet it without onUpdate and it also works for me, really strange ... i also opened another issue related to onUpdate method, not sure it they are somehow related to the same change that happened with v5.

@B4nan B4nan closed this as completed in 9cf454e Feb 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants