Inheritance and polymorphism
Interface inheritance
A graph models "several kinds of thing that share fields" with labels rather than with tables. An
entity declared inheritance: 'interface' is exactly that: a declaration, never a stored node. Its
implementors are the nodes, and matching the interface matches all of their labels at once.
const ProductionSchema = defineEntity({
name: 'Production',
abstract: true,
inheritance: 'interface',
properties: (p) => ({
title: p.string(),
actors: () => p.manyToMany(PersonSchema),
}),
});
const MovieSchema = defineEntity({
name: 'Movie',
extends: ProductionSchema,
labels: ['Movie'],
properties: (p) => ({
released: p.integer(),
actors: () =>
neo4j(p.manyToMany(PersonSchema).owner().pivotEntity(() => ActedInSchema), {
type: 'ACTED_IN',
direction: 'IN',
}),
}),
});
const SeriesSchema = defineEntity({
name: 'Series',
extends: ProductionSchema,
labels: ['Series'],
properties: (p) => ({
episodes: p.integer(),
actors: () =>
neo4j(p.manyToMany(PersonSchema).owner().pivotEntity(() => ActedInSeriesSchema), {
type: 'ACTED_IN',
direction: 'IN',
}),
}),
});
Reading through the interface returns every implementor; reading through an implementor returns only that one:
await em.find(Production, { title: { $like: 'The %' } }); // movies and series
await em.find(Movie, {}); // movies
Each implementor declares its own edge — the same relationship name may be backed by a different
pivot entity per type, which is what @declareRelationship means in the
generated SDL:
interface Production {
title: String!
actors: [Person!]! @declareRelationship
}
type Movie implements Production @node {
released: Int!
actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn")
}
type Series implements Production @node {
episodes: Int!
actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedInSeries")
}
The same shape with decorators:
@Entity({ inheritance: 'interface' })
abstract class Production {
@Property() title!: string;
}
@Entity({ labels: ['Movie', 'Production'] })
class MovieProduction extends Production {
@Property() released!: number;
}
A search field declared on an interface creates one index spanning every implementor's label — fulltext is the only Neo4j index kind that may span labels — and both the interface and each implementor search it.
Multi-label matching
Extra labels are matched, not just written. An entity declared with
labels: ['Employee', 'Manager'] is found by a query for either, which is how a graph expresses the
polymorphism a discriminator column expresses in SQL — without the column.
Remember that indexes are created on the primary label only; a query on a secondary label seeks through the primary one anyway.
Single-table inheritance
discriminatorColumn / discriminatorValue work as core defines them, and the discriminator
survives partial loading: a projection always
keeps the column that decides which entity each row becomes.