In the OOP world, we can create hierarchies of objects. The hierarchy represents objects relationships: it can be an "is-a" or "has-a" relationship. In relational databases, there is no "is-a" relationship, only "has-a" one.
In this topic, we will see how to bring these worlds together and take advantage of OOP when dealing with data stored in a database. Let's learn how to organize our data and apply different strategies to that.
Domain model
In our example, we have a potter's studio. We have a Potter who "is-a" Person. The potter makes Pottery, which may be of two different types: a Plate and a Vase.
Let's focus on the Pottery model:
The image represents the pottery classes hierarchy. But how can we store it in a database? The JPA Specification provides several strategies for that:
Mapped superclass
Table per class
Single table
Joined
Now let's take a closer look at each of them.
Mapped superclass
You can use Mapped superclass to define the state and mapping information that is common to multiple entity classes.
Java
@MappedSuperclass
public class Pottery {
@Id
private Long id;
private String color;
// constructor, getters, setters
}Kotlin
@MappedSuperclass
open class Pottery(
@Id
open var id: Long = 0,
open var color: String = ""
)
Mapped superclass has no @Entity annotation because Pottery is not an entity itself. Therefore, a class cannot have the @Entity and @MappedSuperclass annotations applied to it.
The actual entities which will be stored in the database are Plate and Vase.
Java
@Entity
public class Plate extends Pottery {
private Integer diameter;
// constructor, getters, setters
}@Entity
public class Vase extends Pottery {
private Integer height;
private Integer volume;
// constructor, getters, setters
}Kotlin
@Entity
class Plate(var diameter: Int = 0) : Pottery()@Entity
class Vase(var height: Int = 0, var volume: Int = 0) : Pottery()So, what will our database look like?
Each entity has its own table, both with an id and color columns, and the id is also the primary key of these tables. There is no Pottery table.
The main advantage of this strategy is that it is easy to implement. But this strategy also has a restriction: it's impossible to use polymorphic queries (to select all potteries) because each Pottery is independent. There is no relation between Plate and Vase. We should use another inheritance strategy if we need to use such queries or create a relationship between other entities (e.g., Potter) and all kinds of Pottery (which the Potter made).
Table per class
It's all in the name, isn't it? In this strategy, each concrete (that is, not abstract) class has its own table.
Java
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Pottery {
@Id
private Long id;
private String color;
// constructor, getters, setters
}@Entity
public class Plate extends Pottery {
private Integer diameter;
// constructor, getters, setters
}@Entity
public class Vase extends Pottery {
private Integer height;
private Integer volume;
// constructor, getters, setters
}Kotlin
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
open class Pottery(
@Id
open var id: Long = 0,
open var color: String = ""
)@Entity
class Plate(var diameter: Int = 0) : Pottery()@Entity
class Vase(var height: Int = 0, var volume: Int = 0) : Pottery()And our database looks like this:
Wait, but how is it different from the Mapped superclass strategy? The main difference is that the Pottery class is an entity now, and therefore, it has its own table.
But why do we need it?
This mapping allows us to use polymorphic queries – when we query the base class, the result will contain all the subclasses records. It is achieved by a UNION statement that runs in the background.
Unfortunately, there is no silver bullet in this world, and every efficient strategy has its downsides. For Table per class, they are:
more complex table structure increases the complexity of polymorphic queries;
the background
UNIONrun can lead to performance degradation.
Single table
The Single table strategy is the default strategy in the JPA. In this strategy, there is one table in the database for all classes in the hierarchy. But how can we determine the entity class if we have multiple classes for different entities and there is only one table to store them?
For that purpose, Hibernate provides two annotations:
@DiscriminatorColumndefines a column in the table that determines the entity class in a given database row. The default name of the column is"DTYPE"and has the String type.@DiscriminatorValuedefines the value of the discriminator column for a specific entity.
Java
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn()
public class Pottery {
@Id
private Long id;
private String color;
// constructor, getters, setters
}Kotlin
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn()
open class Pottery(
@Id
open var id: Long = 0,
open var color: String = ""
)@DiscriminatorColumn has name and discriminatorType properties, which allow specifying the discriminator column name and its type respectively.
The specific entity classes are marked with the @DiscriminatorValue annotation:
Java
@Entity
@DiscriminatorValue("plate")
public class Plate extends Pottery {
private Integer diameter;
// constructor, getters, setters
}@Entity
@DiscriminatorValue("vase")
public class Vase extends Pottery {
private Integer height;
private Integer volume;
// constructor, getters, setters
}Kotlin
@Entity
@DiscriminatorValue("plate")
class Plate(var diameter: Int = 0) : Pottery()@Entity
@DiscriminatorValue("vase")
class Vase(var height: Int = 0, var volume: Int = 0) : Pottery()In the database, there will be only one table:
This strategy provides the best performance of the polymorphic queries. No surprise here: all of the entities are kept in one table.
Because all attributes of all entities are presented in the table, there are some drawbacks:
consuming available disk space for the attributes which are not part of any specific entity;
attributes that differ between entities must be marked nullable, which can lead to business logic constraints violation and requires manual handling before storing in a database;
your database administrator will probably have nightmares about data integrity and schema migration issues.
Joined
In the Joined strategy, each class in the hierarchy has its own table. It sounds very similar to the Table per class strategy, but there are two significant differences:
Even abstract classes have their own table.
Each table has columns only for the properties declared in a specific class.
Java
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Pottery {
@Id
private Long id;
private String color;
// constructor, getters, setters
}@Entity
public class Plate extends Pottery {
private Integer diameter;
// constructor, getters, setters
}@Entity
public class Vase extends Pottery {
private Integer height;
private Integer volume;
// constructor, getters, setters
}Kotlin
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
open class Pottery(
@Id
open var id: Long = 0,
open var color: String = ""
)@Entity
class Plate(var diameter: Int = 0) : Pottery()@Entity
class Vase(var height: Int = 0, var volume: Int = 0) : Pottery()Here is the database representation:
This strategy overcomes the drawbacks of the Single table strategy, but at the same time, it loses some performance advantages because of a JOIN statement that runs in the background. The tables are connected with the abstract Pottery table via foreign keys.
Conclusion
There are four inheritance strategies in the JPA. You need to keep in mind the pros and cons of each strategy when choosing the appropriate one.
Use the Mapped superclass strategy if you need to persist entities easily, and you don't need to use polymorphic queries and relationships.
If you need relationships but don't need many polymorphic queries, use the Table per class strategy.
In other cases, you should choose between the Joined strategy and the Single table strategy based on what is more important in your case: data consistency or performance.