The idea: one class tree, three table layouts
Classes inherit; tables don't. When an object-relational mapper saves a class tree, it has to decide
how the classes become tables. The page uses the example from Martin Fowler's Patterns of Enterprise
Application Architecture: an abstract Player with a name, a
Footballer with a club, a Cricketer with a batting_avg,
and a Bowler, a kind of Cricketer, with a bowling_avg.
There are three basic layouts. Every player on the canvas is saved into all three at once.
The trade-off runs between two things: how much table structure is repeated, and how many tables one access has to touch. Each layout is the best one for some operations and the worst for others, and the counters under each panel show it.
Single Table Inheritance
Single Table Inheritance puts the whole tree in one players table with a
column for every field of every class. A type code column (the discriminator: F,
C, B) says which class a row is. When the mapper loads a row, it reads the code
first and builds an object of that class.
- No joins, ever. One row holds one object. Loading all players is one scan (Demo 2), and finding any player by id is one indexed read (Demo 3).
- NULL columns. A Footballer's row has no batting or bowling average, so those cells
are NULL. The six sample players store 10 NULLs in 36 cells; the three of Demo 1 store 5 in 18. Many databases store
NULLs cheaply, but the table is harder to read, and a column that a subclass needs can't be
NOT NULL. - One namespace. Two subclasses can't both have a column called
ratingwith different meanings. Prefix the names with the class. - Refactoring is cheap. Moving a field up or down the class tree changes no table, and
changing an object's class is one
UPDATEof the type code (Demo 7). - One hot table. Every operation reads or writes
players, which collects every index and every lock.
Class Table Inheritance
Class Table Inheritance gives every class its own table with only that class's fields:
players(id, type, name), footballers(id, club), cricketers(id, batting_avg),
bowlers(id, bowling_avg). The rows of one object share its primary key: Bowler 3
is players row 3 + cricketers row 3 + bowlers row 3. A Bowler has a
cricketers row too, because it is a Cricketer. On the canvas the four sample cricketers include the two bowlers.
- The schema mirrors the classes. No NULL columns, no copies; every column means something for every row.
- Joins. Loading a Bowler joins three tables (Demo 3). Loading all players
needs
LEFT JOINs fromplayersto every child table, because most players have no row in most of them. For six players that reads 14 rows, and the outer joins rebuild the very NULL-filled rows that Single Table stores (Demo 2). - The class decides the query. When the class is unknown, the mapper either outer-joins
everything or, as the page does, reads the type code in
playersfirst and then joins the right tables: two statements (Demo 4). - Inserts and deletes touch one table per class on the object's path: 3 for a Bowler. Inserts go parent first and deletes child first, so no foreign key ever points at a missing row (Demo 8).
playersis still touched by every operation.
A variant gives each child table its own key plus a foreign key to players. The shared key is
simpler, and it is what JPA does by default.
Concrete Table Inheritance
Concrete Table Inheritance gives every concrete class one self-contained table
with all its fields, inherited ones included: footballers(id, name, club),
cricketers(id, name, batting_avg), bowlers(id, name, batting_avg, bowling_avg).
Cricketer is concrete but has a subclass, so it gets a table too. That is why the pattern is not called
"one table per leaf". The abstract Player has no table.
- Copied columns.
nameappears in three tables,batting_avgin two. Adding a field to Player alters all three tables (Demo 5). - No joins for a known class.
BowlerMapper.find(3)reads one row frombowlers, and each table is used only when its class is. - "All players" is a UNION. Each branch pads the columns it lacks with NULL (Demo 2). Finding a player of unknown class means trying table after table: 2 statements for a Bowler, 3 for a Cricketer, 3 for an id that doesn't exist (Demo 4).
- Keys must be unique across the tables. If every table numbers its own rows, three
players end up with id 2, and
find(2)returns whichever table it tries first. Use one key table or sequence for the whole hierarchy, or a key made of table name + id (Demo 6). - No foreign key to "a player". A table that references players has no single table to point at.
- Changing the class moves the row. A Cricketer that becomes a Bowler is copied into
bowlersand deleted fromcricketers(Demo 7).
Inheritance Mappers
Whatever the layout, the mapper code has the same shape. The mapper tree mirrors the class
tree: Mapper (insert, update, delete, and the hook methods save and
load) ← AbstractPlayerMapper ← FootballerMapper,
CricketerMapper ← BowlerMapper. Each mapper's save writes its own
class's fields and calls super.save, and load works the same way. A concrete
mapper's find returns its own type, so BowlerMapper.find returns a Bowler without
a downcast.
A separate PlayerMapper, outside the tree, handles calls at the Player level. It keeps one of
each concrete mapper and delegates to the right one. MapperFor tests Footballer, then Bowler,
then Cricketer: Bowler comes before Cricketer because every Bowler is also a Cricketer. On the canvas the
same hook chain runs for all three schemas. Only the tables the hooks write differ, and adding a field
changes exactly one hook (Demo 5).
The same operation, three costs
Statements per operation, starting from the six sample players (Single / Class / Concrete):
| Operation | Single Table | Class Table | Concrete Table |
|---|---|---|---|
| Insert a Bowler | 1 | 3 (one per class) | 1 |
| Load all players | 1 table, 0 joins, 6 rows | 4 tables, 3 outer joins, 14 rows | 3 tables, UNION ALL, 6 rows |
| Find a Bowler, class known | 1 row | 3-table join | 1 row |
| Find a Bowler, class unknown | 1 | 2 (type, then join) | 2 (footballers, bowlers) |
| Find an id that doesn't exist | 1 | 1 | 3 (every table) |
| Add a field to Cricketer | 1 table altered | 1 | 2 |
| Add a field to Player | 1 | 1 | 3 |
| Move a field up or down the tree | no change | 2 tables | every table below |
| A Cricketer becomes a Bowler | 1 UPDATE | UPDATE + INSERT | INSERT + DELETE (row moves) |
| Delete a Bowler | 1 | 3 (child first) | 1 |
The same three in JPA and Hibernate
Jakarta Persistence (JPA) names the three layouts with one annotation on the root entity:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // or JOINED, TABLE_PER_CLASS
@DiscriminatorColumn(name = "type") // default name: DTYPE
public abstract class Player { @Id Long id; String name; }
@Entity @DiscriminatorValue("C") // default value: the entity name
public class Cricketer extends Player { Double battingAvg; }
@Entity @DiscriminatorValue("B")
public class Bowler extends Cricketer { Double bowlingAvg; }
SINGLE_TABLEis the default when@Inheritanceis left out. Without@DiscriminatorColumn, the type column is calledDTYPEand holds the entity name.JOINEDgives each subclass table the parent's primary key as a foreign key (@PrimaryKeyJoinColumnrenames it). It needs no discriminator column. For a polymorphic query Hibernate outer-joins every subclass table and works out the class with aCASEon which join found a row.TABLE_PER_CLASSis optional in the JPA specification, so it isn't portable. Hibernate supports it and answers a polymorphic query with aUNION ALLsubquery padded with NULLs. Hibernate refusesIDENTITYkey generation for it (per-table identity columns can't keep keys unique across the tables), so use a sequence or a table generator. That is Demo 6's problem.- A loaded entity can't change its class. To turn a Cricketer into a Bowler you delete it and persist a new Bowler (a new id), or change the rows with native SQL and clear the persistence context.
@MappedSuperclassis a fourth option: the parent's fields are copied into each entity's table, as in Concrete Table, but the parent is not an entity, so there are no polymorphic queries or associations to it.
See also Hibernate Session: Persistence Context, Dirty Checking and Flush for how these entities are loaded and flushed, and How PostgreSQL Executes JOIN, GROUP BY and ORDER BY for what the outer joins cost.
Choosing, and mixing
| Layout | Good at | Bad at |
|---|---|---|
| Single Table | no joins, one place to look, refactoring, changing class | NULL columns, one big table, no NOT NULL on subclass fields |
| Class Table | clean, normalized schema; foreign keys to any class | joins on every load, outer joins for polymorphic queries, the hot root table |
| Concrete Table | self-contained tables, one-table reads of a known class | copied columns, UNION for polymorphic queries, keys unique across tables, no foreign key to the parent |
Single Table is a common first choice: it is simple and survives most refactorings. The others come in when the NULL columns or the size of the one table start to hurt. The three can be mixed in one hierarchy, for example a single table for most classes and a table of its own for one class with many fields, at the price of more complex mapping code. Class Table is the normalized layout (see Database Normalization: 1NF, 2NF and 3NF); Single Table trades NULLs for having no joins. On the object side, deep class trees bring their own problems (see Inheritance vs composition, side by side).
What the page leaves out
Real mappers keep an identity map and a unit of work, load lazily, batch statements and cache. The page counts statements and rows, not time, and leaves out indexes, locking and contention on the root table, and how a database compresses NULLs. It doesn't show mixed layouts in one hierarchy, interfaces and multiple inheritance, Class Table with its own keys plus foreign keys, compound keys of table name + id, discriminator formulas, or polymorphic associations from other tables.
On the mind map
References
Martin Fowler: Patterns of Enterprise Application Architecture (2002), chapters 3 and 12
Fowler's catalog: Single Table Inheritance, Class Table Inheritance, Concrete Table Inheritance, Inheritance Mappers
Jakarta Persistence 3.1 specification: inheritance mapping strategies, @DiscriminatorColumn