Skip to content

Persisted getter are causing unexpected update queries [postgresql] #6723

@thdk

Description

@thdk

Describe the bug

We've noticed some unexpected update queries are being executed for persisted getters on the entity.

@Entity()
class User {
  @Property({nullable: true, type: JsonType})
  children?: Child[];

  @Property({ type: ArrayType, nullable: true, persist: true, hydrate: false })
  get childEmails(): string[] | undefined {
    return this.children?.map((c) => c.email.toLowerCase()).filter((e) => !!e);
  }
}

Simply fetching this data for this entity from the database would mark the childEmails as dirty and make an update query on the next flush.

While making the reproduction, I've noted that it doesn't occur when using the mysql driver.

(Code below is taken from the reproduction)

Sample entity:

import { ArrayType, Entity, JsonType, PrimaryKey, Property } from '@mikro-orm/postgresql';

export type Child = {
  email: string;
}
@Entity()
export class User {

  @PrimaryKey()
  id!: number;

  @Property({nullable: true, type: JsonType})
  children?: Child[];

  @Property({ type: ArrayType, nullable: true, persist: true, hydrate: false })
  get childEmails(): string[] | undefined {
    return this.children?.map((c) => c.email.toLowerCase()).filter((e) => !!e);
  }
}

Mysql test

import { MikroORM } from '@mikro-orm/sqlite';
import { User } from './entity';

let orm: MikroORM;

beforeAll(async () => {
  orm = await MikroORM.init({
    dbName: ':memory:',
    entities: [User],
    debug: ['query', 'query-params'],
    allowGlobalContext: true, // only for testing
  });
  await orm.schema.refreshDatabase();

  orm.em.create(User, {
    children: [{
      email: 'test@example.com'
    }],
  });
  await orm.em.flush();
  orm.em.clear();

  const connection = orm.em.getConnection();
  dbExecuteSpy = jest.spyOn(connection, 'execute');
});

let dbExecuteSpy: jest.SpyInstance;

beforeEach(() => {
  dbExecuteSpy.mockClear();
});

afterAll(async () => {
  await orm.close(true);
});

test('should not try to persist persisted getter if its value has not changed', async () => {

  await orm.em.findAll(User);

  await orm.em.flush();
  
  expect(dbExecuteSpy).toHaveBeenCalledTimes(1);

  expect(dbExecuteSpy).toHaveBeenNthCalledWith<any[]>(
    1,
    'select `u0`.* from `user` as `u0`',
    [],
    'all',
    undefined,
    { id: 1 },
  );
});

Postgress test

import { MikroORM, defineConfig } from '@mikro-orm/postgresql';
import { User } from './entity';

let orm: MikroORM;

beforeAll(async () => {
  orm = await MikroORM.init(defineConfig({
    dbName: 'postgres',
    entities: [User],
    debug: ['query', 'query-params'],
    allowGlobalContext: true, // only for testing
  }));
  await orm.schema.refreshDatabase();

  orm.em.create(User, {
    children: [{
      email: 'test@example.com'
    }],
  });
  await orm.em.flush();
  orm.em.clear();

  const connection = orm.em.getConnection();
  dbExecuteSpy = jest.spyOn(connection, 'execute');
});

let dbExecuteSpy: jest.SpyInstance;

afterAll(async () => {
  await orm.close(true);
});

test('should not try to persist persisted getter if its value has not changed', async () => {
  dbExecuteSpy.mockClear();

  await orm.em.findAll(User);

  await orm.em.flush();
  
  expect(dbExecuteSpy).toHaveBeenCalledTimes(2); // This is unexpected, it should be 1

  expect(dbExecuteSpy).toHaveBeenNthCalledWith<any[]>(
    1,
    'select \"u0\".* from \"user\" as \"u0\"',
    [],
    'all',
    undefined,
    { id: 1 },
  );

  // This is the additional query that is being done
  expect(dbExecuteSpy).toHaveBeenNthCalledWith<any[]>(
    2,
    'update \"user\" set \"child_emails\" = ? where \"id\" = ?',
    [
        "{test@example.com}",
        1
    ],
    'run',
    expect.anything(),
    { id: undefined },
  );
});

note: reasoning for creating a bug ticket instead of a discussion is the different behavior between the mysql and postgresql driver.

Reproduction

https://github.com/thdk/reproduction/tree/feat/add-test-case

What driver are you using?

@mikro-orm/postgresql

MikroORM version

6.4.17-dev.20

Node.js version

22.2.0

Operating system

mac

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions