Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Relation hydration silently dropped when entity constructor pre-initializes the relation property with a class instance (regression in 1.0 via #11267) #12683

Copy link
Copy link

Description

@bredar
Issue body actions

Description

Since TypeORM 1.0, RelationMetadata.setEntityValue() merges the loaded relation
entity into an already-set relation property instead of assigning it
(introduced in #11267 to prevent eager-loaded relations from overwriting
manually requested nested relations):

// RelationMetadata.setEntityValue
if (ObjectUtils.isObject(entity[propertyName])) {
    OrmUtils.mergeDeep(entity[propertyName], value);
} else {
    entity[propertyName] = value;
}

However OrmUtils.merge only merges plain objects (isPlainObject
guard, see #7774):

static merge(target, source, memo = new Map()) {
    if (OrmUtils.isPlainObject(target) && OrmUtils.isPlainObject(source)) { ... }
    ...
}

Loaded relation values are always class instances (entity instances), and
when the target property already holds a class instance too, mergeDeep is a
silent no-op: the freshly hydrated relation entity is discarded and the stale
pre-set instance stays on the parent entity.

This breaks any entity whose constructor pre-initializes a relation property,
e.g.:

@Entity()
class Post {
    constructor() {
        this.author = new Author(); // sensible default for "create" flows
    }

    @ManyToOne(() => Author)
    @JoinColumn({ name: 'authorId' })
    author: Author;
}

// typeorm creates entities via the constructor during hydration (unless
// entitySkipConstructor is set), so `author` is already an Author instance...
const post = await repo.findOne({ where: {}, relations: { author: true } });
post.author; // ...and stays the EMPTY Author() from the constructor.
             // All hydrated Author columns are silently dropped.

On 0.3.x this worked: setEntityValue assigned unconditionally.

Expected Behavior

relations: { author: true } returns the hydrated Author entity, as in 0.3.x.

Actual Behavior

post.author is the empty instance created in the constructor; every column
of the loaded relation row is silently discarded (the SELECT contains the
columns; only the entity transformation drops them).

Discussion / possible fixes

Note the constraint: the #11265 scenario currently relies on the no-op —
when the same to-one relation is hydrated twice (manual nested join first,
eager join second), the silent no-op is what preserves the deeper first
value. So a naive "assign when target is a class instance" would fix this
issue but re-open #11265 for that ordering.

Possible directions:

  1. Distinguish "pre-existing value was set by this hydration run" from
    "pre-existing value came from the user/constructor". E.g. the transformer
    marks relation properties it has already written (a Symbol on the
    entity, or a per-run Set<entity, propertyPath>); first write assigns
    (restoring 0.3.x semantics for constructor presets), subsequent writes
    merge/no-op (keeping Eager relations with DeleteDateColumn override manually requested relations with nested properties #11265 behavior).

  2. Make the merge class-instance aware: when target and value are instances
    of the same entity, merge column values (and only overwrite relation
    properties that are undefined on the target). Heavier, touches
    OrmUtils.

Option 1 seems minimal and semantically precise. Happy to open a PR with a
regression test if you agree with a direction.

Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    Morty Proxy This is a proxified and sanitized view of the page, visit original site.