Mikro-orm: Missing properties when using findAll({ populate: true })

Created on 23 Nov 2020  路  6Comments  路  Source: mikro-orm/mikro-orm

Describe the bug
When you have a relation and you perform findAll({ populate: true }) and the parent has the same property name as the child property the child property is absent.

Stack trace

[Nest] 8448   - 2020-11-23 20:06:06   [MikroORM] [query] begin
[Nest] 8448   - 2020-11-23 20:06:06   [MikroORM] [query] insert into `child` (`property`) values ('test') [took 3 ms]
[Nest] 8448   - 2020-11-23 20:06:06   [MikroORM] [query] insert into `parent` (`property_id`) values (1) [took 1 ms]
[Nest] 8448   - 2020-11-23 20:06:06   [MikroORM] [query] commit
[Nest] 8448   - 2020-11-23 20:06:10   [MikroORM] [query] select `e0`.* from `parent` as `e0` [took 1 ms]
[Nest] 8448   - 2020-11-23 20:06:10   [MikroORM] [query] select `e0`.* from `child` as `e0` where `e0`.`id` in (1) order by `e0`.`id` asc [took 1 ms]

Result

[
  {
    "id": 1,
    "property": {
      "id": 1
    }
  }
]

To Reproduce
Steps to reproduce the behavior:

Perform findAll() method on parent controller.

Expected behavior
The property for the child to be populated and not absent.

[
  {
    "id": 1,
    "property": {
      "id": 1,
      "property": "test"
    }
  }
]

Additional context

@Entity()
export class Child {
  @PrimaryKey()
  id: number;

  @Property()
  property: string;
}

@Entity()
export class Parent {
  @PrimaryKey()
  id: number;

  @ManyToOne(() => Child)
  property: Child;
}

@Injectable()
export class ParentController {
  constructor(@InjectRepository(Parent) private repository: EntityRepository<Parent>) {
  }

  create(body) {
    const entity = this.repository.create(body);
    this.repository.persist(entity);
    return entity;
  }

  async findAll() {
    console.log(await this.repository.findAll({ populate: true }));
  }
}

@Injectable()
export class ChildController {
  constructor(@InjectRepository(Child) private repository: EntityRepository<Child>) {
  }

  create(body) {
    const entity = this.repository.create(body);
    this.repository.persist(entity);
    return entity;
  }
}

@Injectable()
export class EntityController {
    constructor(
        private orm: MikroORM,
        private parentController: ParentController,
        private childController: ChildController
    ) {}

    async create() {
        const child = this.childController.create({
            property: 'test',
        });

        this.parentController.create({
            property: child
        });

        await this.orm.em.flush();
    }
}

Versions

| Dependency | Version |
| - | - |
| node | 12.18.4 |
| typescript | 4.0.3 |
| nestjs | 7.0.0 |
| mikro-orm | 4.3.1 |
| mikro-orm/mysql | 4.3.1 |
| mikro-orm/nestjs | 4.2.0 |

bug

All 6 comments

And your code looks like what? Entity definitions are not enough.

One thing to note right from the start - populate: true will not follow recursion, you need to handle that manually.

Updated my psuedo example. I use NestJS. I have to add that I do not have this issue with TypeORM. I like MikroORM because of the select statement it does per relation instead of one giant JOIN.

This is still not enough, I need a reproduction - that includes some data as well. Prepare a script that will first add the data so it is clearly visible what you are up to. I need actual code, not pseudocode, and I need to clearly see what and where you think should behave differently and how. Ideally it should look like existing reproductions here: https://github.com/mikro-orm/mikro-orm/tree/master/tests/issues (everything in a single file, and include assertions).

But as I already noted, populate: true will not follow recursive relations.

Also your entity definitions are weird, parent entity has m:1 relation to child? So you have multiple parents that can point to a single child? Sounds a bit up side down, isn't it?

As requested a working test case. Hope this explains my issue better.


import { Entity, Logger, ManyToOne, MikroORM, PrimaryKey, Property } from '@mikro-orm/core';
import { AbstractSqlDriver } from '@mikro-orm/knex';

@Entity()
export class B {

  @PrimaryKey()
  id!: number;

  @Property()
  property!: string;

}

@Entity()
export class A {

  @PrimaryKey()
  id!: number;

  @ManyToOne(()=> B)
  property!: B;

}

describe('GH issue 1115', () => {

  let orm: MikroORM<AbstractSqlDriver>;
  const log = jest.fn();

  beforeAll(async () => {
    orm = await MikroORM.init({
      entities: [A, B],
      dbName: ':memory:',
      type: 'sqlite',
    });
    const logger = new Logger(log, ['query', 'query-params']);
    Object.assign(orm.config, { logger });

    await orm.getSchemaGenerator().createSchema();
    const b = orm.em.create(B, {
      property: 'foo',
    });
    const a = orm.em.create(A, {
      property: b,
    });
    await orm.em.persistAndFlush(a);
    orm.em.clear();
  });

  beforeEach(() => {
    log.mockReset();
    orm.em.clear();
  });

  afterAll(() => orm.close(true));

  test('findAll({ populate: true }) should return all properties on child even when it has the same name in the parent', async () => {
    const user = await orm.em.find(A, { id: 1 }, { populate: true });
    const data = JSON.parse(JSON.stringify(user));

    await expect(data[0].property).toContain({ id: 1, property: 'foo' });
  });
});

Great, that helps a lot, thanks!

So the issue is about serialization, and it is caused by the property name being same (A.property vs A.b.property). I guess I know what is happening, this was done as a prevention to infinite recursion, but the heuristic is too loose. If you use different property name, it does work as expected.

Fixed in 4.3.2-dev.5

Was this page helpful?
0 / 5 - 0 ratings