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 |
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