question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Propagation of ManyToOne back to collections

See original GitHub issue

I’m writing some business logic / tests that creates / updates entities based on the ManyToOne side of a relation, and later looks / asserts against the OneToMany side of that same relation, but it is empty.

I.e.:

  it("should update posts", () => {
    const author = new Author();
    const post = new Post();
    post.author = author.toReference();
    expect(author.posts.getItems()).toContain(post);
  });

At first I thought this was a bug in getItems() thinking that the collection was initialized when really it was not. However, that is not the case, the collection is initialized (as empty since Author is a new entity), it’s just not being kept up-to-date as the post.author field changes.

This means that to correctly have author.posts.getItems() to have the correct value, I have to: a) flush the author and post entities to the db, b) reload the author entity and c) init its collection.

This turns the unit of work / entities / collections into a leakier abstraction, as now I need to realize collections might return invalid values that don’t respect the work-in-progress that exists in the rest of my unit of work. (I.e. I think ideally the unit of work is supposed to represent a unified / update-to-date view of currently-loaded entities, i.e. instead of re-fetching a row from the db everytime and missing “oh we’ve changed the author’s name in this UoW so use that value”).

So, I’d like to see this “we have some WIP and everyone sees that WIP consistently” behavior of the unit of work extended from primitives to these collections.

…all that said, it looks like the docs already highlight this propagation works if relationships are setup through the Collection.add/remove methods, I’d just like to see the same thing happen on the other side, so that my code doesn’t have to be coupled to “to get the most consistent behavior out of Mikro, we always need to modify relationships via the Collection side”.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:8 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
B4nancommented, Feb 21, 2020

So I finally cracked all of the problems that this change uncovered. They were all there but not visible as one usually do not make the relation fully propagated in the entity graph.

As part of the refactorings I had to introduce commit order calculator, pospone m:n collection queries, also there were bugs in EntityFactory.

Will try to split the work into multiple commits now so all the changes are visible in the history, but finally all tests are now passing.

0reactions
B4nancommented, Jan 22, 2020

Also, if you did introduce a “must be loaded” check to child.parent accesses via a defineProperty getter, then I think that would mean use cases like await child.parent.init() would not work?

Not sure what you mean, getter would always return, only setter in this case would propagate the change when the inverse side (1:m collection) is initialized (as you cannot modify collection that is not).

Already got this implemented and it works fine, with a downside of console.log(entity) not printing getter properties by default. That can be solved by inspect.custom, but it’s kinda tricky to implement it correctly (computing correct padding for nested objects). Now I am trying to use this also for 1:1, but there few failing tests.

Here is the code for patching the prototype:

  private static defineReferenceProperties<T extends AnyEntity<T>>(meta: EntityMetadata<T>) {
    Object
      .values<EntityProperty>(meta.properties)
      .filter(prop => [ReferenceType.ONE_TO_ONE, ReferenceType.MANY_TO_ONE].includes(prop.reference) && (prop.inversedBy || prop.mappedBy))
      .forEach(prop => {
        Object.defineProperty(meta.prototype, prop.name, {
          set(val: AnyEntity) {
            if (!('__data' in this)) {
              Object.defineProperty(this, '__data', { value: {} });
            }

            EntityHelper.defineReferenceProperty(prop, this, val);
          },
        });
      });
  }

  private static defineReferenceProperty<T extends AnyEntity<T>>(prop: EntityProperty<T>, ref: T, val: AnyEntity) {
    Object.defineProperty(ref, prop.name, {
      get() {
        return this.__data[prop.name];
      },
      set(val: AnyEntity) {
        this.__data[prop.name] = val;
        const inverse = val && val[prop.inversedBy || prop.mappedBy];

        if (prop.reference === ReferenceType.MANY_TO_ONE && inverse && inverse.isInitialized()) {
          (inverse as Collection<T>).add(this);
        }

        if (prop.reference === ReferenceType.ONE_TO_ONE && val && wrap(val).isInitialized() && inverse !== this) {
          val[prop.inversedBy || prop.mappedBy] = this;
        }
      },
      enumerable: true,
      configurable: true,
    });
    ref[prop.name] = val as T[string & keyof T];
  }
Read more comments on GitHub >

github_iconTop Results From Across the Web

The best way to map a @OneToMany relationship with JPA ...
This is the most natural way of mapping a database one-to-many database association, and, usually, the most efficient alternative too.
Read more >
JPA: OneToMany relationship keeps the Collection empty
The best way to map a @OneToMany association is to rely on the @ManyToOne side to propagate all entity state changes:
Read more >
Junit Test - @OneToMany relationship returning the collection ...
REQUIRED and Propagation.SUPPORTS when there is no transaction the first starts a new transaction and the last one executes non-transactionally ...
Read more >
Most efficient way to map a @OneToMany relationship with ...
The best way to map a @OneToMany association is to rely on the @ManyToOne side to propagate all entity state changes. So, the...
Read more >
Collections | MikroORM
OneToMany and ManyToMany properties are stored in a Collection wrapper. ... initialize collection if not already loaded and return its items as array...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found