首页 > 解决方案 > TypeORM 延迟加载更新父在子保存时失败

问题描述

我不确定这是否是一个错误,或者我做错了什么,但我尝试了很多事情来让它工作,但我做不到。我希望你们能提供帮助。

基本上我有一对一的关系,我需要lazyLoad。关系树在我的项目中有点大,我无法在没有承诺的情况下加载它。

我面临的问题是,当我保存孩子时,父更新生成的 sql 缺少更新字段:UPDATE `a` SET WHERE `id` = 1

当我不使用lazyLoading(Promises)时,这非常有效。

我使用生成的代码工具设置了一个简单的示例。

实体 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>;
}

实体 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));

输出

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]

如果我从实体中删除承诺,一切正常:

实体 A

...
    @OneToOne(
        (type: any) => B,
        (o: B) => o.a
    )
    @JoinColumn()
    public b: B;
}

实体 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);
...

输出

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`

我还创建了一个 git 项目来说明这一点并便于测试。

1)使用承诺(不工作)https://github.com/cuzzea/bug-typeorm/tree/promise-issue

2)没有延迟加载(工作)https://github.com/cuzzea/bug-typeorm/tree/no-promise-no-issue

标签: typescriptlazy-loadingone-to-onetypeorm

解决方案


promise-issue我在您的存储库分支中闲逛了一下,发现了一些有趣的事情:

  1. 无效UPDATE查询由初始触发await aRepo.save(aCreated);,而不是由插入B和后续外键分配触发a.b。在避免问题a.b = null之前分配。aRepo.create(a)

  2. a.b = null;在之前添加初始化aRepo.create(a)避免了意外的无效UPDATE;IE:

    const a = new A();
    a.name = "something";
    a.b = null;
    const aCreated = aRepo.create(a);
    await aRepo.save(aCreated);
  3. 我相当有信心将async函数用于(ie. ) 的inverseSide参数是不正确的。文档表明这应该是 just ,并且泛型也证实了这一点。TypeORM 将在将值传递给此函数之前解析承诺,并且函数返回另一个而不是正确的属性值。@OneToOne()async (o: B) => await o.a)
    (o: B) => o.aOneToOne
    asyncPromise

  4. 我也刚刚注意到您正在传递一个class Ato的实例aRepo.create()。这不是必需的;您可以将您的实例直接传递给aRepo.save(a). 只需将Repository.create()提供的对象中的值复制到实体类的新实例中。.create()当它们不存在时,它似乎也会创建承诺。这实际上可能是导致此问题的原因;aCreated在调用之前的日志记录aRepo.save(aCreated)表明承诺没有解决。
    事实上,删除aRepo.create(a)步骤(并将 save 更改为await aRepo.save(a);也似乎可以避免这个问题。Repository<T>.create()当它的参数已经是时,也许正在以不同的方式处理延迟加载属性instanceof T?我会调查一下。

我也尝试将typeorm包升级到typeorm@next(0.3.0-alpha.12) 但问题似乎仍然存在。

我刚刚注意到您已经为此记录了一个GitHub 问题;在接下来的几天里,我将着眼于创建一个测试用例来演示。

我希望这足以回答你的问题!

更新

经过进一步的代码跟踪后,上面列表中的第 4) 项似乎是导致此问题的原因。

其中RelationLoader.enableLazyLoad(),TypeORM 使用自己的 getter 和 setter 重载 @Entity 实例上的惰性属性访问器 - 例如Object.defineProperty(A, 'b', ...)。重载的属性访问器加载并缓存相关B记录,返回Promise<B>.

Repository.create()迭代创建的实体的所有关系,并且 - 当提供对象时 - 从提供的值构建新的相关对象。但是,此逻辑不考虑Promise对象,而是尝试直接从 Promise 的属性构建相关实体。

所以在你上面的例子中,aRepo.create(a)构建一个新的A,迭代A关系(即),并从onb构建一个空的。新的没有定义任何属性,因为实例不共享任何属性。然后因为没有指定,所以没有为 定义外键名和值,导致你遇到的错误。BPromisea.bBPromiseBidaRepo.save()

因此,在这种情况下,简单地a直接传递aRepo.save()并删除该aRepo.create(a)步骤似乎是正确的做法。

然而,这是一个应该解决的问题——但我认为这不是一个容易解决的问题,因为这确实需要Repository.create()能够实现awaitPromise;目前无法实现,因为Repository.create()它不是异步的。


推荐阅读