Mixing inheritance strategies in Hibernate

27 September 2011, by Baptiste Autin

Inheritance does not exist in the entity-relationship model as such.
In a relational database, one can represent inheritance relationship:

  • Either with two tables and an intermediary association (joined strategy in JPA)
  • Either with a single table in which the two entities have merged, along with an additional column to discriminate the name of the entity (single_table strategy)

A variant of the latter strategy also exists (table per class), but I ignore it for simplicity (it consists in defining one table per concrete class, gathering the attributes of the concrete class as well as those of all the superclasses).

Both of these strategies has advantages and disadvantages in terms of performance, ease of use, scalability, compliance / non-compliance with normal forms…

Hibernate supports both techniques, and can even mix them.
It is that mixing inheritance strategy that I am going to talk to you now.

Use case

I have taken my inspiration from this article, a bioinformatics paper that proposes an ontology of clinical terms associated with neoplasms.
I will not dwell on ontologies, nor on tumors ; I am just taking the classification scheme provided by the author as an example.

From this scheme, I have extracted the following class diagram:

Let’s assume now that these classes have some attributes and methods (not listed).

Let’s also assume that the top class (Neoplasm) and the middle classes (NeuralCrest, GermCell, Mesoderm, Trophectoderm, Neuroectoderm and EndodermEctoderm) are abstract.
And the other ones are concrete.

Now imagine that we want to persist instances of concrete classes in a relational database.
How are we going to proceed?
And first, how are we going to map this object model to a physical database model?

To start, it is likely that some of these classes will share common attributes (a latin name, a family, a biological category, a set of associated vocabularies, etc.)
Contrarily, the bottom-level classes will probably have specific very attributes.
For instance:

Creating one big single table “neoplasm” including all attributes of all classes, along with an additional field to discriminate the class name, is not very elegant… even if that solution is probably a winner in terms of performance (no table joins needed). I am saying “probably” because it depends on how the database will handle a large number of columns with NULL values. Moreover, indexing a column of that big table means indexing all the recordings of that table… including, potentially, unnecessary ones.

Creating one table per concrete class, with as many foreign keys as there are inheritance relationships, is a classic solution… But with 26 classes, we need 26 tables.
Moreover, if bottom classes have few attributes of their own, is it really necessary to create dedicated tables only for them?
Couldn’t we limit the number of tables by combining some tables, locally?

That’s where mixing inheritance strategies comes in handy.

I suggest the following:
– Each top and middle class will be mapped to a dedicated table
– Bottom classes will be merged in the middle class they inherit

For example, classes Molar and Tropoblast will be grouped in the table Tropectoderm, classes NeuroectodermPrimitive and NeuralTube in the table Neuroectoderm, etc.

The table definition for Trophectoderm then becomes: trophectoderm (id, a, b, c, d, e, f, type)
Field type is the type discriminator of the class hierarchy (we need one, since we have merged some classes). By default, Hibernate automatically populates this field with the simple name of the Java classes.

Mapping

We set a “single table” strategy on the class Neoplasm :

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

Every second level class declares a secondary table :

@SecondaryTable(name="NeuralCrest", pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))

Third level classes use both the secondary table AND the global table (Neoplasm).

Note that JPA annotations are not automatically inherited, and therefore must explicitly be set in all the subclasses.
Also note that it is necessary to redefine the table name in all the @Column annotations that refer to a secondary table:

@Column (table = TABLE_NAME)
private String s;

That’s why my code makes use of a public constant TABLE_NAME, in order to define in one single place the name of the mapped table.

Care should be taken to create a referential integrity constraint on the primary key of each table towards the primary key of Neoplasm (ie Trophectoderm.id -> Neoplasm.id, NeuralCrest.id -> Neoplasm.id, etc..). This will prevent deleting from a secondary table without deleting the associated row in Neoplasm.

Using the API

We can now make persistent our objects. For example:

NeuralCrestPrimitive NeuralCrestPrimitive ncp = new ();
ncp.setLatinName ("Pia mater");
ncp.setA ("Example # 1");
ncp.setB ("Example # 2");
session.persist (ncp);

Hibernate then executes the following two commands:
insert into neoplasm (latinName, type) values ??('Pia Mater', 'NeuralCrestPrimitive')
insert into neural_crest (a, b, id) values ??(Example # 1 Example # 2 [last inserted id])

Conclusion
We mapped our object model to a database model containing 7 tables, instead of 26 tables with the usual joined strategy.
A read operation in a bottom table requires only one table join, a write operation two inserts (or updates).
It is a compromise solution, which is particularly interesting:
– When the height of the hierarchical tree is large (at least 3 levels)
– When the bottom classes structuraly differs little from one class to another within the same branch
– When the class structure is very different from one branch to another

Java classes

// *********
// Top class
// *********

@Entity
@Table(name = Neoplasm.TABLE_NAME)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
abstract public class Neoplasm {

	public static final String TABLE_NAME = "neoplasm";

	@Id @GeneratedValue
	private int id;
	
	@Column(name = "latinName")
	private String latinName;
	
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getLatinName() {
		return latinName;
	}
	public void setLatinName(String latinName) {
		this.latinName = latinName;
	}
}

// **************
// Middle classes
// **************

@Entity
@SecondaryTable(name=NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class NeuralCrest extends Neoplasm {
	
	public static final String TABLE_NAME = "neural_crest";
	
	@Column(table=TABLE_NAME)
	private String a;
	
	@Column(table=TABLE_NAME)
	private String b;
	
	public String getA() {
		return a;
	}
	public void setA(String a) {
		this.a = a;
	}
	public String getB() {
		return b;
	}
	public void setB(String b) {
		this.b = b;
	}
}

@Entity
@SecondaryTable(name = GermCell.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class GermCell extends Neoplasm {
	
	public static final String TABLE_NAME = "germ_cell";
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class Mesoderm extends Neoplasm {
	
	public static final String TABLE_NAME = "mesoderm";
}

@Entity
@SecondaryTable(name = Neuroectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class Neuroectoderm extends Neoplasm {
	
	public static final String TABLE_NAME = "neuroectoderm";
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class EndodermEctoderm extends Neoplasm {
	
	public static final String TABLE_NAME = "endoderm_ectoderm";
}

@Entity
@SecondaryTable(name = Trophectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
abstract public class Trophectoderm extends Neoplasm {
	
	public static final String TABLE_NAME = "trophectoderm";
}

// **************
// Bottom classes
// **************

@Entity
@SecondaryTable(name = NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Ectomesenchymal extends NeuralCrest {
	
	@Column(table=TABLE_NAME)
	private String d;
	
	public void setD(String d) {
		this.d = d;
	}

	public String getD() {
		return d;
	}
}

@Entity
@SecondaryTable(name = NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class NeuralCrestEndocrine extends NeuralCrest {
	
	@Column(table=TABLE_NAME)
	private String d;
	
	@Column(table=TABLE_NAME)
	private String e;

	public void setD(String d) {
		this.d = d;
	}
	public String getD() {
		return d;
	}
	public void setE(String e) {
		this.e = e;
	}
	public String getE() {
		return e;
	}
}

@Entity
@SecondaryTable(name = NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class NeuralCrestPrimitive extends NeuralCrest {
		
	@Column(table = TABLE_NAME)
	private String d;

	public void setD(String d) {
		this.d = d;
	}

	public String getD() {
		return d;
	}
}

@Entity
@SecondaryTable(name = NeuralCrest.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class PeripheralNervousSystem extends NeuralCrest {
	
	@Column(table=TABLE_NAME)
	private String c;
	
	@Column(table=TABLE_NAME)
	private String f;
	
	@Column(table=TABLE_NAME)
	private String g;
	
	public String getC() {
		return c;
	}
	public void setC(String c) {
		this.c = c;
	}
	public String getF() {
		return f;
	}
	public void setF(String f) {
		this.f = f;
	}
	public String getG() {
		return g;
	}
	public void setG(String g) {
		this.g = g;
	}
}


@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Coelomic extends Mesoderm {
}

@Entity
@SecondaryTable(name = GermCell.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Differentiated extends GermCell {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class EndodermEctodermEndocrine extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class EndodermEctodermPrimitive extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Mesenchymal extends Mesoderm {
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class MesodermPrimitive extends Mesoderm {
}

@Entity
@SecondaryTable(name = Trophectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Molar extends Trophectoderm {
}


@Entity
@SecondaryTable(name = Neuroectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class NeuralTube extends Neuroectoderm {
}

@Entity
@SecondaryTable(name = Neuroectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class NeuroectodermPrimitive extends Neuroectoderm {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Odontogenic extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Parenchymal extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = GermCell.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Primordial extends GermCell {
}

@Entity
@SecondaryTable(name = Mesoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Subelomic extends Mesoderm {
}

@Entity
@SecondaryTable(name = EndodermEctoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Surface extends EndodermEctoderm {
}

@Entity
@SecondaryTable(name = Trophectoderm.TABLE_NAME, pkJoinColumns = @PrimaryKeyJoinColumn(name="id"))
public class Trophoblast extends Trophectoderm {
}

Variant
Rather than letting Hibernate save textually the class name in the discriminator column, we could define an entity NeoplasmType(id, name), and set a foreign key in Neoplasm for this entity.
We then must not forget to define the corresponding numerical identifier in every concrete class, with the annotation @DiscriminatorValue.

For example, the mapping of the class Trophectoderm becomes:

@Entity
@SecondaryTable(name="trophectoderm", pkJoinColumns=@PrimaryKeyJoinColumn(name="id"))
@DiscriminatorValue("3")	// si "3" est l'identifiant numérique correspondant à "Trophectoderm" dans la table NeoplasmType 
abstract class Trophectoderm extends Neoplasm {
	(...)
}

We must also specify in Neoplasm that Hibernate should not rely on the class name to discriminate the entity class, but on a numeric identifier and a table join:

@Entity
@Table(name = Neoplasm.TABLE_NAME)
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "id_type", discriminatorType = DiscriminatorType.INTEGER)
abstract public class Neoplasm {
	(...)
	@ManyToOne
	@JoinColumn(name="id_type", nullable=false, insertable=false, updatable=false)
	private NeoplasmType type;
	(...)
}

4 commentaires sur “Mixing inheritance strategies in Hibernate”

  1. Andrey says:

    Is it possible to express that mapping on XML?

  2. Baptiste Autin says:

    I think you could do something like (though not tested):

    <hibernate-mapping>
        <class name=”Neoplasm” table=”neoplasm”>
            <id name=”id” column=”id” type=”int”>
                <generator class=”native”/>
            </id>
            <discriminator column=”type” type=”string”/>
            <subclass name=”NeuralCrest”>
                <join table=”neural_crest”>
                    <key column=”id”/>
                    <property name=”a” column=”a”/>
                </join> 
                <subclass name=”Ectomesenchymal”>
                </subclass>
                <subclass name=”NeuralCrestPrimitive”>
                    <property name=”d” column=”d”/>
                    <property name=”e” column=”e”/>
                </subclass>
                <subclass name=”PeripheralNervousSystem”>
                    <property name=”c” column=”c”/>
                    <property name=”f” column=”f”/>
                    <property name=”g” column=”g”/>
                </subclass>
            </subclass>
            <subclass name=”GermCell”>
                <join table=”germ_cell”>
                    <key column=”id”/>
                    (…)
                </join>
            </subclass>
            <subclass name=”Mesoderm”>
                <join table=”neuroectoderm”>
                    <key column=”id”/>
                    (…)
                </join>
            </subclass>
        </class>
    </hibernate-mapping>

  3. Sergio says:

    Hi, I have tried this map pattern for my three level hierarchy case. When I save a bottom object I get an error (PK constraint) because hibernate does three inserts: 1 insert into table A, 1 insert into table B and 1 insert into table B again with differents properties set (specific for bottom object). I have revised annotations and class definitions and I don’t see any difference with your example. Do you have any idea what may be happening? Thank you!!

  4. Pierrick says:

    Bonjour,
    merci pour cet exemple simple et détaillé.
    Avez-vous essayé avec un niveau supplémentaire (4). Dans mon cas j’ai une classe abtraite supplémentaire, et je me trouve confronté à un problème de contrainte de clé unique.
    Merci pour votre réponse
    Cdlt, Pierrick

Laisser une réponse

«     »