1

Starting from default npx typeorm init project:

@Entity()
export class A {
    @PrimaryGeneratedColumn()
    id!: number

    @OneToMany(() => B, (b) => b.a)
    bs: B[]

    constructor(bs: B[]) {
        this.bs = bs
    }
}
@Entity()
export class B {
    @PrimaryGeneratedColumn()
    id!: number

    @ManyToOne(() => A, (a) => a.bs)
    a: A

    constructor(a: A) {
        this.a = a
    }
}
AppDataSource.initialize().then(async () => {

    const a = await AppDataSource.manager.save(new A([]))
    await AppDataSource.manager.save(new B(a))
    await AppDataSource.manager.save(new B(a))
    await AppDataSource.manager.save(new B(a))
    console.log(a.bs.length)

}).catch(error => console.log(error))

This displays 0 (a.bs is still empty).

I would expect that a.bs is hydrated with the saved B's. Isnt that the whole purpose of supplying an inverse mapping on the @ManyToOne anyway?

Or, if I'm getting this wrong, what's the proper way of maintaining DB<->model consistency when adding relationed entities?

Rahul Sharma
  • 5,562
  • 4
  • 24
  • 48
Dinu
  • 1,374
  • 8
  • 21
  • You could just add `a.bs.push(this)` in the `B` constructor. – jona303 Oct 19 '22 at 07:20
  • @jona303 I could, but what about relations that are queried levels-deep on a join? Should I populate levels of the relation? Should I know all the instances? Then I can't delegate model operations. This is the exact mess that an ORM should get you out of... – Dinu Oct 19 '22 at 17:31
  • The Manager knows all the instances and their state (new, committed, hydrated etc) and the ORM knows all the relations. Why duplicate it in imperative code? ORM should be declarative. – Dinu Oct 19 '22 at 17:39

1 Answers1

1

Your mapping definitions are correct. The issue is that the object a was initialised with empty array for the values of bs and so a.bs.length returns 0.

While you created multiple records of entity B afterwards. Object a isn't aware of those. Typeorm won't trace or find all entities in your project and refresh them now that some relations/mappings in the database has changed. In order to refresh the mappings, you'll need to reload a from the database:

await a.reload();

so that a now knows about the updated mappings.

Rahul Sharma
  • 5,562
  • 4
  • 24
  • 48
  • Do you know if there is - or is planned - an efficient representation of `.reload()` that won't make a useless trip to the DB? This is anyway awkward enough, as an ORM is supposed to abstract relations... if I have to know about all (even transitory) relations of B and their instances to reload, it's a mess... – Dinu Oct 14 '22 at 08:38
  • An ORM is just an abstraction so that you write your code in your language and not SQL. Any ORM out there won't do this for you. You can read [this](https://stackoverflow.com/a/1279678/1973735) for starters. – Rahul Sharma Oct 14 '22 at 17:15
  • While it may not strictly fall under the ORM concept, as far as my experience goes (I come from the glorious world of PHP, quite new to the node ecosystem) it's almost a must for the concept of ORM + Entity Manager. For implementing concepts as lazy commit, lazy hydration etc, a Manager MUST keep the relationships consistent at all times and SHOULD NOT rely on DB constraints to achieve that by means of preventive failure. If I can inconsistently change both sides of the relation, this requirement fails. Also, it's ludicrously easy to implement, so I was kinda relying on it. – Dinu Oct 15 '22 at 18:35
  • But alas, your answer is painfully correct in thet TypeORM does not provide it. – Dinu Oct 15 '22 at 18:36
  • Example for why this is crucially important: `b1 = new B(a); b2 = new B(a); a.bs = [b1]`: Now, the relation is unstable and the end result depends on the order that I save a, b1, b2, and this is a consistentency silent failure (it will not generate any error, but can equally produce a => (b1,b2), a=>(b1) in the DB. While the ORM abstracts data, the Manager should abstract operations. – Dinu Oct 15 '22 at 18:46