Skip to main content

Relationship entities

In Neo4j a relationship can hold properties. Model that as an entity marked relationship, and use it as the pivotEntity of a many-to-many:

import { Collection } from 'mikro-orm-neo4j';
import { Entity, ManyToMany, ManyToOne, PrimaryKey, Property } from '@mikro-orm/decorators/legacy';

@Entity()
export class Actor {
@PrimaryKey() id!: string;
@Property() name!: string;

@ManyToMany(() => Movie, undefined, {
owner: true,
pivotEntity: () => ActedIn,
inversedBy: 'actors',
relationship: { type: 'ACTED_IN', direction: 'OUT' },
})
movies = new Collection<Movie>(this);
}

@Entity()
export class Movie {
@PrimaryKey() id!: string;
@Property() title!: string;

@ManyToMany(() => Actor, (actor) => actor.movies)
actors = new Collection<Actor>(this);
}

The pivot is where the edge properties live:

@Entity({ relationship: { type: 'ACTED_IN' } })
export class ActedIn {
@ManyToOne(() => Actor, { primary: true })
actor!: Actor;

@ManyToOne(() => Movie, { primary: true })
movie!: Movie;

// Stored on the relationship, not on either node.
@Property()
roles!: string[];

@Property({ nullable: true })
billing?: number;
}
MATCH (a:Actor) MATCH (m:Movie) WHERE a.id = $sId AND m.id = $tId
MERGE (a)-[r:ACTED_IN]->(m)
SET r.roles = $roles, r.billing = $billing

With defineEntity​

const ActedInSchema = defineEntity({
name: 'ActedIn',
relationship: { type: 'ACTED_IN' },
properties: (p) => ({
actor: () => p.manyToOne(ActorSchema).primary(),
movie: () => p.manyToOne(MovieSchema).primary(),
roles: p.array('string'),
}),
});

Rules​

  • Exactly two reference properties. A relationship entity has one property per endpoint (@ManyToOne or @OneToOne); anything else fails discovery by name, because an edge has two ends.
  • The type comes from the declaration, or from the entity name uppercased when relationship is just true.
  • Endpoints are matched on their full primary key — every column of a composite key, and each endpoint's own schema where one applies. Matching on a partial key used to let an edge fan out to another tenant's node; see the C11 appendix.
  • Indexes on a relationship entity are edge indexes: FOR ()-[r:ACTED_IN]-() ON (r.billing). Fulltext is the exception — see full-text search.
Why relationship rather than a custom @Rel() decorator?

Relying on MikroORM's own PropertyOptions and EntityOptions keeps the declaration inside the lifecycle the ORM already runs — discovery, metadata caching, schema generation — instead of requiring a second scan of custom decorators, which is where reflection-based extraction gets brittle.