TypeORM 延迟加载更新 parent 在 child 保存时失败

TypeORM lazyload update parent fails on child save

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

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

我面临的问题是,当我保存 child 时,生成的 parent 更新 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

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

  1. 无效的 UPDATE 查询是由初始 await aRepo.save(aCreated); 触发的,而不是由 B 的插入和随后对 [=15= 的外键分配触发的].在 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) => await o.a))是不正确的.
    The documentation 表示这应该只是 (o: B) => o.aOneToOne 上的泛型也证实了这一点。
    TypeORM 将在它通过之前解析承诺此函数的值,以及一个 async 函数 returns 另一个 Promise 而不是正确的 属性 值。

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

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

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

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

更新

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

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

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

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

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

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

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

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

正如此处和 corresponding issue, when using create passing in a Promise for a lazy loaded relation field simply does not work despite the type signature of that function demanding otherwise (and despite the documentation suggesting 上指出的那样,延迟加载的字段应该包含在 Promise.resolve 中以便保存)。什么似乎是根据 @Timshel 在上述 PR 中的评论是:

nasty TypeScript type casts when assigning object literals to lazy-load properties

这意味着使用 create 方法,如果您为这些延迟加载的字段之一传入普通实体对象(而不是包含所述对象的 Promise),typeorm 实际上会正确设置此值你将能够保存。稍后访问此字段时,您甚至会神奇地获得承诺。上面的引述提到,您可以通过在将实体对象传递给创建之前将其强制转换为 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的类型签名。我自己不需要这样做,但是我敢肯定,只要付出足够的努力,就可以用同样的方法来解决这个问题。