Make Inheritance Look Like an Association, Not a Copy
The fix for the table-per-subclass problem, why it makes an ORM's default inheritance mapping so much cheaper to query, and the one real case where an ORM needs to be told to skip that fix entirely.
Counting every pen, across every subtype, meant querying four separate tables and adding the results together. That cost doesn't stay fixed — it compounds.
The Cost Compounds With Every New Subclass
Picture a Player base class with an id and a name, and a Bot subclass adding a difficultyLevel. Finding the name of everyone who's ever played means querying two tables today.
Add a third kind of player, an OnlinePlayer with its own rank and region, and that same query now needs a third table. Every query written against this hierarchy before that point has to be revisited and updated, not just the new ones, because none of them knew the new subtype's table existed.
The Fix: One Shared Table, Joined by ID
The fix is to stop duplicating shared attributes into every subclass's table at all. The parent gets exactly one table, holding every attribute every subtype shares. Each subclass gets its own table too, but that table holds only the attributes unique to it, plus a foreign key pointing back to the parent's row.
Applied to the pen hierarchy from before:
pens:id,colorball_pens:ballDiameter,penIdgel_pens:inkViscosity,penIdfountain_pens:nibWidth,penId
Counting every pen, of any kind, is now a single query against a single table: SELECT COUNT(*) FROM pens. Getting every player's name works the same way, SELECT name FROM players, no matter how many player subtypes exist by then.
This is why an ORM handles inheritance the same way it handles a normal relationship between two classes. Each child table gets a foreign key pointing back to the parent table. It does not copy the parent's columns into every child table.
The One Case This Doesn't Fit: A Base Model Nobody Ever Queries
This same fix runs into trouble the moment it meets the shared base model every entity in this design already extends, the one holding just an id, createdAt, and updatedAt.
Follow the pattern exactly as described, and every single entity in the system, City, Theatre, Show, all of them, would need a foreign key pointing back to one shared base_models table. That's a real cost for a concept nobody ever actually queries. Someone asks "show me every player," a real, meaningful question a business needs answered. Nobody ever asks "show me every base model."
A join that exists only to fetch an ID and two timestamps, on every table in the entire system, gives nothing a business ever asked for. What's actually needed here is different from the pen and player cases: these shared fields should sit directly inside each child's own table, not in a separate table reached through a foreign key.
Telling the ORM to Skip the Fix Entirely
The way to say that is to mark the base class itself as a mapped superclass rather than an entity. That single label tells the ORM two things at once: never create a table for this class on its own, and copy its attributes directly into whichever table extends it.
The effect shows up immediately. Every entity that previously complained about a missing primary key stops complaining, because id, createdAt, and updatedAt are no longer reached through a join to some shared table — they're columns sitting directly on that entity's own table, the same as any attribute it declared itself.
Keep reading