Typeorm Lazyload Update父级在子级保存上失败

时间:2018-08-29 01:14:23

标签: typescript lazy-loading one-to-one typeorm

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

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

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

当我不使用lazyLoading(承诺)时,这是完美的工作方式。

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

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

2 个答案:

答案 0 :(得分:2)

我在您的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;即:

    const a = new A();
    a.name = "something";
    a.b = null;
    const aCreated = aRepo.create(a);
    await aRepo.save(aCreated);
  3. 我非常有信心,将async的{​​{1}}参数(即inverseSide)使用@OneToOne()函数是不正确的。
    The documentation表示这应该只是async (o: B) => await o.a),并且(o: B) => o.a上的泛型也可以确认这一点。
    TypeORM将在将值传递给此函数之前解决诺言,并且OneToOne函数返回另一个async而不是正确的属性值。

  4. 我也刚刚注意到您正在将Promise的实例传递给class A。这不是必需的。您可以将实例直接传递到aRepo.create()aRepo.save(a)只需将提供的对象中的值复制到实体类的新实例中。看来Repository.create()会在不存在时创建承诺。这实际上可能是导致此问题的原因。在调用.create()之前记录aCreated表示未兑现承诺。
    实际上,删除aRepo.save(aCreated)步骤(并将保存更改为aRepo.create(a)似乎也可以避免此问题。也许await aRepo.save(a);在其参数已经为{{1}时正在以不同的方式处理惰性加载属性}?我会调查这个。

我还尝试将Repository<T>.create()软件包升级到instanceof T(0.3.0-alpha.12),但是问题似乎仍然存在。

我刚刚注意到您已经为此登录了GitHub issue;我将在接下来的几天中创建一个测试用例进行演示。

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

更新

在进一步的代码跟踪之后,看来上面列表中的项目4)是导致此问题的原因。

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

B迭代创建实体的所有关系,并且-在提供对象时-根据提供的值构建新的相关对象。但是,此逻辑不考虑Promise<B>对象,而是尝试直接从Promise的属性中构建相关的实体。

因此,在上述情况下,Repository.create()构建新的Promise,迭代aRepo.create(a)关系(即A),并构建空的A来自b上的B。新的Promise没有定义任何属性,因为a.b实例没有共享任何属性B。然后,由于未指定Promise,因此未为B定义外键名称和值,从而导致您遇到错误。

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

但是,这是一个应解决的问题-但我认为这不是一个简单的解决方法,因为这确实需要aRepo.save()才能aRepo.create(a)兑现承诺;由于Repository.create()异步,目前无法实现。

答案 1 :(得分:0)

跟进@Timshel 的精彩回答(并尝试解决 typeorm 本身的潜在问题)。

对于那些在这里寻找替代 https://github.com/typeorm/typeorm/pull/2902 被合并的解决方法的人来说,我想我已经想出了一些办法(假设您正在使用带有 typeorm 的 ActiveRecord 模式)。首先总结一下,因为文档中缺少大部分信息,需要从各种 github 问题/这个 SO 问题中拼凑起来:

正如此处和 corresponding issue 所指出的,当使用 create 为延迟加载的关系字段传递一个 Promise 时,尽管该函数的类型签名要求否则(并且尽管文档 suggesting 说明延迟加载的字段应包含在 Promise.resolve 中以进行保存)。什么似乎根据 @Timshel 在上述 PR 中的评论是:

<块引用>

将对象字面量分配给延迟加载属性时,TypeScript 类型转换令人讨厌

这意味着,使用 create 方法,如果您为这些延迟加载的字段之一传入一个普通实体对象(而不是包含所述对象的 Promise),typeorm 实际上会正确设置此值,并且您将能够保存。稍后访问此字段时,您甚至会神奇地得到一个承诺。上面的引述提到,您可以通过在将实体对象传递给 create 之前强制将实体对象强制转换为 Promise 来利用这一点。但这需要根据具体情况来完成,如果您不小心遵守了类型签名而不是强制转换,您将在运行时得到意想不到的结果。如果我们可以纠正这个类型签名,让编译器只在我们以一种不起作用的方式使用这个函数时对我们大喊大叫,这不是很好吗?我们可以,方法如下:)

import {
  BaseEntity,
  DeepPartial,
  ObjectType,
} from 'typeorm';

/**
 * Conditional type that takes a type and maps every property which is
 * a Promise to the unwrapped value of that Promise. Specifically to correct the type
 * of typeorm's create method. Using this otherwise would likely be incredibly unwise.
 *
 * For example this type:
 * {
 *   hey: number,
 *   thing: Promise<ThingEntity>,
 *   sup: string
 * }
 *
 * gets mapped to:
 * {
 *   hey: number,
 *   thing: ThingEntity,
 *   sup: string
 * }
 *
 */
type DePromisifyValue<T> = T extends Promise<infer U> ? U : T;
type DePromisifyObject<T> = T extends object
  ? { [K in keyof T]: DePromisifyValue<T[K]> }
  : T;

export abstract class CommonEntity extends BaseEntity {
  static create<T extends CommonEntity>(
    this: ObjectType<T>,
    entityLike?: DeepPartial<DePromisifyObject<T>>
  ): T {
    if (!entityLike) {
      return super.create<T>();
    }
    return super.create<T>(entityLike as DeepPartial<T>);
  }
}

这样做是定义此 create 方法的覆盖版本,该方法接受与原始 create 方法相同的对象参数,除非任何 Promise 字段存在解包版本(即myLazyLoadedUser: Promise<UserEntity> 变成 myLazyLoadedUser: UserEntity)。然后将其传递到原始 create 方法中,并强制将其强制转换为具有所有 Promise 字段的旧版本,就像 BaseEntity 喜欢的方式(或说谎喜欢的方式)。在 typeorm 本身没有解决问题的情况下,在某些时候无法避免强制转换,但此解决方案只需要在一个中心位置强制转换,我们可以确信我们正在做正确的事情。只需扩展此 CommonEntity(随意命名)而不是 BaseEntitycreate 方法将需要您的正确类型。无需将您的值包装在 Promise.resolve 中。并且从它返回的实体仍将具有原始 Promise 类型的那些延迟加载的字段。

注意:我还没有处理传入对象数组的 create 的类型签名。我自己不需要这个,但我确定用同样的方法打字,只要努力就可以搞定。