【问题标题】:TypeORM lazyload update parent fails on child saveTypeORM 延迟加载更新父在子保存时失败
【发布时间】:2018-08-29 01:14:22
【问题描述】:

我不确定这是否是一个错误,或者我做错了什么,但我尝试了很多方法来让它工作,但我做不到。希望大家帮忙。

基本上我有一对一的关系,我需要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>;

}

ma​​in.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;

}

ma​​in.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

【问题讨论】:

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


    【解决方案1】:

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

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

    2. aRepo.create(a)之前添加初始化a.b = null;避免了意想不到的无效UPDATE;即:

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

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

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

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

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

    更新

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

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

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

    因此,在上述情况下,aRepo.create(a) 构建一个新的A,迭代A 关系(即b),并从Promise 上的a.b 构建一个空的B。新的B 没有定义任何属性,因为Promise 实例不共享任何属性B。那么因为没有指定id,所以没有为aRepo.save()定义外键名和值,导致你遇到的错误。

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

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

    【讨论】:

      【解决方案2】:

      跟进@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&lt;UserEntity&gt; 变为@987654333 @)。然后它将它传递给原始的create 方法,并强制将其转换为具有所有Promise 字段的旧版本,就像BaseEntity 喜欢的方式(或谎称喜欢那样)。如果没有在 typeorm 本身内解决问题,就无法避免在某些时候强制强制转换,但这种解决方案只需要在一个中心位置强制强制转换,我们可以确信我们正在做正确的事情。只需扩展这个CommonEntity(随便你怎么称呼它)而不是BaseEntitycreate 方法将需要你的正确类型。无需将您的值包装在 Promise.resolve 中。并且从它返回的实体仍将具有那些带有原始 Promise 类型的延迟加载字段。

      注意:我没有处理 create 的类型签名,其中传入了一个对象数组。我自己不需要这个,但我确定用同样的方法,只要付出足够的努力就可以解决。

      【讨论】:

        猜你喜欢
        • 2015-03-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-18
        • 2014-04-03
        • 2011-08-27
        • 2018-02-27
        • 1970-01-01
        相关资源
        最近更新 更多