Skip to main content

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;
}
One fulltext index across implementors

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.