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
(
@ManyToOneor@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
relationshipis justtrue. - 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.