I am not sure if this is a bug, or I am doing something wrong, but I tried a lot of things to get this working and I couldn't. I hope you guys can help.
Basically I have a one to one relationship that I need to lazyLoad. The relation tree is kind of big in my project and I can't load it without promises.
The issue I face is that when I save a child, the parent update generated sql is missing the update fields: UPDATE `a` SET WHERE `id` = 1
This is working perfectly when I am not using lazyLoading (Promises).
I got a simple example set up using the generated code tool.
Entity A
@Entity()
export class A {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(
(type: any) => B,
async (o: B) => await o.a
)
@JoinColumn()
public b: Promise<B>;
}
Entity B
@Entity()
export class B {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(
(type: any) => A,
async (o: A) => await o.b)
a: Promise<A>;
}
main.ts
createConnection().then(async connection => {
const aRepo = getRepository(A);
const bRepo = getRepository(B);
console.log("Inserting a new user into the database...");
const a = new A();
a.name = "something";
const aCreated = aRepo.create(a);
await aRepo.save(aCreated);
const as = await aRepo.find();
console.log("Loaded A: ", as);
const b = new B();
b.name = "something";
const bCreated = bRepo.create(b);
bCreated.a = Promise.resolve(as[0]);
await bRepo.save(bCreated);
const as2 = await aRepo.find();
console.log("Loaded A: ", as2);
}).catch(error => console.log(error));
Output
Inserting a new user into the database...
query: SELECT `b`.`id` AS `b_id`, `b`.`name` AS `b_name` FROM `b` `b` INNER JOIN `a` `A` ON `A`.`bId` = `b`.`id` WHERE `A`.`id` IN (?) -- PARAMETERS: [[null]]
query: START TRANSACTION
query: INSERT INTO `a`(`id`, `name`, `bId`) VALUES (DEFAULT, ?, DEFAULT) -- PARAMETERS: ["something"]
query: UPDATE `a` SET WHERE `id` = ? -- PARAMETERS: [1]
query failed: UPDATE `a` SET WHERE `id` = ? -- PARAMETERS: [1]
If I remove the promises from the entities, everything is working fine:
Entity A
...
@OneToOne(
(type: any) => B,
(o: B) => o.a
)
@JoinColumn()
public b: B;
}
Entity B
...
@OneToOne(
(type: any) => A,
(o: A) => o.b)
a: A;
}
main.ts
createConnection().then(async connection => {
...
const bCreated = bRepo.create(b);
bCreated.a = as[0];
await bRepo.save(bCreated);
...
Output
query: INSERT INTO `b`(`id`, `name`) VALUES (DEFAULT, ?) -- PARAMETERS: ["something"]
query: UPDATE `a` SET `bId` = ? WHERE `id` = ? -- PARAMETERS: [1,1]
query: COMMIT
query: SELECT `A`.`id` AS `A_id`, `A`.`name` AS `A_name`, `A`.`bId` AS `A_bId` FROM `a` `A`
I have also created a git project to illustrate this and for easy of testing.
1) using promises (not working) https://github.com/cuzzea/bug-typeorm/tree/promise-issue
2) no lazy loading (working) https://github.com/cuzzea/bug-typeorm/tree/no-promise-no-issue
I've had a poke around in your promise-issue
repository branch, and discovered a few interesting things:
The invalid UPDATE
query is being triggered by the initial await aRepo.save(aCreated);
, NOT by the insert of B
and subsequent foreign key assignment to a.b
. Assigning a.b = null
before the aRepo.create(a)
avoids the issue.
Adding the initialization a.b = null;
before aRepo.create(a)
avoids the unexpected invalid UPDATE
; ie:
const a = new A();
a.name = "something";
a.b = null;
const aCreated = aRepo.create(a);
await aRepo.save(aCreated);
I'm fairly confident that the use of async
functions for the inverseSide
argument to @OneToOne()
(ie. async (o: B) => await o.a)
) is incorrect.
The documentation indicates that this should be just (o: B) => o.a
, and the generics on OneToOne
also confirm this.
TypeORM will be resolving the promise BEFORE it passes the value to this function, and an async
function returns another Promise
rather than the correct property value.
I've also just noticed that you're passing an instance of class A
to aRepo.create()
. This isn't necessary; you can pass your instance directly to aRepo.save(a)
. The Repository.create()
simply copies the values from the provided object into a new instance of the entity class. It also appears that .create()
creates promises when they don't already exist. This might actually be the cause of this issue; logging aCreated
just before aRepo.save(aCreated)
is called shows that the promise isn't resolved.
In fact, removing the aRepo.create(a)
step (and changing the save to await aRepo.save(a);
also seems to avoid this issue. Perhaps Repository<T>.create()
is handling lazy load properties differently when its argument is already instanceof T
? I'll look into this.
I also tried upgrading the typeorm
package to typeorm@next
(0.3.0-alpha.12) but the issue still appears to exist there.
I've just noticed you've already logged a GitHub issue for this; I'll look at creating a test case to demonstrate over the next few days.
I hope this is enough to answer your question!
After some further code tracing, it appears that item 4) from the list above is the reason for this issue.
In RelationLoader.enableLazyLoad()
, TypeORM overloads lazy property accessors on @Entity instances with its own getter and setter - e.g. Object.defineProperty(A, 'b', ...)
. The overloaded property accessors load and cache the related B
record, returning Promise<B>
.
Repository.create()
iterates all relations for the created entity and - when an object is provided - builds a new related object from the provided values. However, this logic doesn't account for a Promise
object, and tries to build a related entity directly from the Promise's properties.
So in your case above, aRepo.create(a)
builds a new A
, iterates the A
relations (ie. b
), and builds an empty B
from the Promise
on a.b
. The new B
doesn't have any properties defined because Promise
instances don't share any properties B
. Then because there is no id
specified, the foreign key name and value are not defined for aRepo.save()
, leading to the error you've encountered.
So simply passing a
directly to aRepo.save()
and removing the aRepo.create(a)
step seems the correct course of action in this case.
This is an issue that should be fixed however - but I don't think it's an easy fix since this really needs for Repository.create()
to be able to await
the Promise; which not achievable at present since Repository.create()
is not async.