Skip to main content

Class Table Inheritance

Joist supports Class Table Inheritance, which allows inheritance/subtyping of entities (like class Dog extends Animal), by automatically mapping single/logical polymorphic entities across separate per-subtype/physical SQL tables.

Database Representation

For example, lets say we have a Dog entity and a Cat entity, and we want them to both extend the Animal entity.

For class table inheritance, we represent this in Postgres by having three separate tables: animals, dogs, and cats.

  • The animals table has an id primary key, with the usual auto increment behavior, and any fields that are common to all Animals
  • The dogs table also has an id primary key, but it does not auto-increment, and is instead a foreign key to animals.id, and has any fields that are unique to the Dog entity
  • The cats table also has an id primary key, again it does not auto-increment, and is instead a foreign key to animals.id, and has any fields that are unique to the Cat entity

If you're using Joist's migration-utils, this might look like:

createEntityTable(b, "animals", {
name: "text",
})
createSubTable(b, "animals", "dogs", {
can_bark: "boolean",
});
createSubTable(b, "animals", "cats", {
can_meow: "boolean",
});

Entity Representation

When joist-codegen sees that dogs.id is actually a foreign key to animals.id, Joist will ensure that the Dog model extends the Animal model.

Note that because of the codegen entities, which contain the getter/setter boilerplate, it will actually end up looking like:

// in AnimalCodegen.ts
abstract class AnimalCodegen extends BaseEntity {
name: string;
}

// in Animal.ts
class Animal extends AnimalCodegen {
// any custom logic
}

// in DogCodegen.ts
abstract class DogCodegen extends Animal {
can_bark: boolean;
}

// in Dog.ts
class Dog extends DogCodegen {
// any custom logic
}

And when you load several Animals, Joist will automatically probe the dogs and cats tables (by using a LEFT OUTER JOIN to each subtype table) and create entities of the right type:

const [a1, a2] = await em.loadAll(Animal, ["a:1", "a:2"]);
// If a1 was saved as a dog, it will be a Dog
expect(a1).toBeInstanceOf(Dog);
// if a2 was saved as a cit, it will be a Cat
expect(a2).toBeInstanceOf(Cat);

Similarly, if you save a Dog entity, Joist will automatically split the entity's data across both tables, putting the name into animals and can_bark into dogs, with the same id value for both rows:

const dog = em.create(Dog, {
name: "doge",
can_bark: true,
});
// Generates both `INSERT INTO animals ...` and
// `INSERT INTO dogs ...`.
await em.flush();

Tagged Ids

Currently, subtypes share the same tagged id as the base type.

For example, dog1.id returns a:1 because the Dog's base type is Animal, and all Animals (regardless of whether they're Dogs or Cats) use the a tag.

Joist might someday support per-subtype tags, but it would be complicated b/c we don't always know the subtype of an id; e.g. if there is a pet_owners.animal_id foreign key, and it points to either Dogs or Cats, when loading the row PetOwner:123 it's impossible to know if the tagged id its animal_id value should be d:1 or c:1 without first probing the dogs and cats tables, which takes extra SQL calls to do. So for now it's simplest/most straightforward to just share the same tag across the subtypes.

Abstract Base Types

If you'd like to enforce that base type is abstract, i.e. that users cannot instantiate Animal, they must instantiate either a Dog or Cat, then you can mark Animal as abstract in the joist-config.json file:

{
"entities": {
"Animal": {
"tag": "a",
"abstract": true
}
}
}

You also need to manually update the Animal.ts file to make the class abstract:

export abstract class Animal extends AnimalCodegen {}

After this, Joist will enforce that all Animals must be either Dogs or Cats.

For example, if an em.load(Animal, "a:1") finds a row only in the animals table, and no matching row in the dogs or cats table, then the em.load method will fail with an error message.

But Isn't Inheritance Bad Design?

Yes, inheritance can be abused, particularly with deep inheritance hierarchies and/or just bad design decisions.

But when you have a situation that fits it well, it can be an appropriate/valid way to design a schema, at your own choice/discretion.

If it helps, inheritance can also be thought of Abstract Data Types, which as a design pattern is generally considered a modern/good approach for accurately & type-safely modeling values that have different fields based on their current kind/type.

ADTs also focus just on the per-kind/per-type data attributes, and less on the polymorphic behavior of methods encoded/implemented within the class hierarchy which was the focus of traditional OO-based inheritance.

When using inheritance with Joist entities, you can pick whichever approach you prefer: either more "just data" ADT-ish inheritance or "implementation-hiding methods" OO-ish inheritance.