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 Player, Footballer, Cricketer and Bowler class tree on top. Below it, three players Minh, Raj and Ali stored three ways: one players table with a type column and NULL cells; four tables players, footballers, cricketers and bowlers whose rows share the id; three tables footballers, cricketers and bowlers in which name and batting_avg are copied.
The same three players: one wide table with NULLs, four tables tied together by the id, or three self-contained tables with copied columns.

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 rating with 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 UPDATE of 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 from players to 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 players first 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).
  • players is 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. name appears in three tables, batting_avg in two. Adding a field to Player alters all three tables (Demo 5).
  • No joins for a known class. BowlerMapper.find(3) reads one row from bowlers, 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 bowlers and deleted from cricketers (Demo 7).
Left: footballers, cricketers and bowlers each number their rows 1 and 2, so find(2) matches three rows. Right: one key table hands out 1 to 6, so every id is in exactly one table. Bottom: a charity_functions table whose player_id has no single players table to reference.
With one counter per table, three different players share key 2; one key table for the hierarchy fixes that, but no foreign key can point at "any player".

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).

Inserting a Bowler: PlayerMapper delegates to BowlerMapper; the save hooks BowlerMapper.save, CricketerMapper.save and AbstractPlayerMapper.save each write their fields. A grid shows where each hook's fields go: all into the players row in Single Table, into bowlers, cricketers and players in Class Table, all into the bowlers row in Concrete Table.
One chain of save hooks, three destinations: one row, one row per class, or one row in the concrete class's table.

The same operation, three costs

Statements per operation, starting from the six sample players (Single / Class / Concrete):

OperationSingle TableClass TableConcrete Table
Insert a Bowler13 (one per class)1
Load all players1 table, 0 joins, 6 rows4 tables, 3 outer joins, 14 rows3 tables, UNION ALL, 6 rows
Find a Bowler, class known1 row3-table join1 row
Find a Bowler, class unknown12 (type, then join)2 (footballers, bowlers)
Find an id that doesn't exist113 (every table)
Add a field to Cricketer1 table altered12
Add a field to Player113
Move a field up or down the treeno change2 tablesevery table below
A Cricketer becomes a Bowler1 UPDATEUPDATE + INSERTINSERT + DELETE (row moves)
Delete a Bowler13 (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_TABLE is the default when @Inheritance is left out. Without @DiscriminatorColumn, the type column is called DTYPE and holds the entity name.
  • JOINED gives each subclass table the parent's primary key as a foreign key (@PrimaryKeyJoinColumn renames it). It needs no discriminator column. For a polymorphic query Hibernate outer-joins every subclass table and works out the class with a CASE on which join found a row.
  • TABLE_PER_CLASS is optional in the JPA specification, so it isn't portable. Hibernate supports it and answers a polymorphic query with a UNION ALL subquery padded with NULLs. Hibernate refuses IDENTITY key 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.
  • @MappedSuperclass is 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

LayoutGood atBad at
Single Tableno joins, one place to look, refactoring, changing classNULL columns, one big table, no NOT NULL on subclass fields
Class Tableclean, normalized schema; foreign keys to any classjoins on every load, outer joins for polymorphic queries, the hot root table
Concrete Tableself-contained tables, one-table reads of a known classcopied 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

This page on the Software Design mind map · 28 pages in 7 branches · open it to see the whole map and test yourself in recall mode.