Hibernate Inheritance, Composite PK
Hibernate Inheritance, Discriminator Column, Composite PK, EmbededId
Greetings, everyone; today, we will discuss the Hibernate Inheritance and Composite PK. I am going to publish a couple of articles about Hibernate. This is the third one. Let’s refresh our coffees and get started! ☕ 📰😊
Hibernate Inheritance is a method of reflecting and creating tables into OOP classes according to Table or Column. For instance, you might have two different machine accessor types, root accessor and regular accessor, and you want to separate these two types from each other. How can you handle this? There are two ways to provide this: you can describe a column and separate users with a query. On the other hand, you can prefer using Hibernate Inheritance. I am going to talk about MappedSuperClass, Discriminator, Single and Joined.
MappedSupperClass
This type is used to capsulate common values in one class. It has no Entity annotation because this is not an entity. It just involves common values for each sub-class, such as BaseEntity; you might have some data that contains sub-classes; you can put them together at a parent class using a @MappedSuperClass annotation.
Example:
import lombok.Data;
import javax.persistence.*;
import java.time.OffsetDateTime;
@Data
@MappedSuperclass
public abstract class BaseEntity{
private static final Boolean DEFAULT_DELETED = false;
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.SEQUENCE)
protected Integer id;
@Column(name = "created_at", nullable = false, updatable = false)
protected OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false)
protected OffsetDateTime updatedAt;
protected Boolean deleted = DEFAULT_DELETED;
@Version
@Column(name = "version")
protected Long version;
@PrePersist
public void prePersist() {
this.createdAt = OffsetDateTime.now();
this.updatedAt = OffsetDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = OffsetDateTime.now();
}
}
Product Class
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
}
SINGLE TABLE
Hibernate creates one table for each entity that is signed with @Inheritance annotation and as an InheritanceType.SINGLE_TABLE. When the client calls the repository, hibernate maps related to the columns to the related class. This feature could be very useful in some cases. For instance, you have public and private machines, and you want to separate two machines from each other. Each machine has its own properties. In this case, the single table will be entirely useful.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Machine {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
public class PublicMachine extends Machine {
// other prop
}
@Entity
public class PrivateMachine extends Machine {
// other prop
}
Discriminator Column and Value
The discriminator is such a helpful feature for most cases in Hibernate. For instance, you have two accessor_type users, such as root and regular. You want to retrieve them from the database and separate different classes with different values.
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "accessor_type", discriminatorType = DiscriminatorType.STRING)
public class MachineAccessor {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String ipAddress;
private String macAddress;
}
@Entity
@DiscriminatorValue("regular_machine")
public class RegularMachineAccessor extends MachineAccessor {
private String ippAddress;
private String macAddress;
private List<String> logHistory;
}
@Data
@Entity
@DiscriminatorValue("root_machine")
public class RootMachineAccessor extends MachineAccessor {
private String rootPassword;
private String rootUsername;
private String rootIpAddress;
private String rootMacAddress;
}
JOINED TABLE
In this strategy, each class in the hierarchy is mapped to its own table in the database. The tables are linked together through foreign key relationships to represent the inheritance relationships between the classes. The superclass table contains common properties, while the subclass tables contain properties specific to each subclass.
In the kingdom of E-commerce, the Payment entity serves as the foundation for all transactions, with attributes such as ID, amount, currency, and method. Specialized payment provider clans, like the BitcoinProviders and PaypalProviders, exist, each with unique attributes like bitcoinAddress and bitcoinWallet for Bitcoin providers and paypalEmail and paypalPassword for PayPal providers. Despite their differences, all providers are connected through the Joined Table inheritance strategy, ensuring a seamless and secure payment experience for all.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private BigDecimal amount;
private String currency;
private String method;
}
@Entity
public class BitcoinProvider extends Payment {
private String bitcoinAddress;
private String bitcoinWallet;
private String bitcoinProviderName;
}
@Entity
@PrimaryKeyJoinColumn(name = "paypal_id")
public class PaypalProvider extends Payment {
private String paypalEmail;
private String paypalPassword;
private String paypalProviderName;
}
COMPOSITE PK (Composite Identifier)
There is another way in hibernate to define a primary key, combining two columns as a primary key. This phenomenon is called Composite PK or Composite Identifier. In hibernate, Composite PK is defined using @EmbeddedId and @Embeddable annotation. This allows us to provide some benefits, such as increasing simplicity and performance; on the other hand, it comes with complexity and maintenance problems.
There is no exact proof that Composite Primary Keys are useless or worse. It completely depends on the requirements. I will give the best usage as much as I can.
For instance, you have users whose emails are encrypted, providing some regular government policies, and each user has a different user type that is created by the admin, like the username, so that you cannot directly search for these users; therefore, you can use composite PK to search users according to their user type. That’s why uniqueness will be provided, and searches will be made easier. Obviously, direct searching by email will be difficult. We can use composite PK combining email and user type.
create table "user" (
email varchar(255) not null,
user_type varchar(255) not null,
name varchar(255),
primary key (email, user_type)
)
UserID
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public class UserId implements Serializable {
private String email;
private String userType;
}
USER
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
public class User {
@EmbeddedId
private UserId userId;
private String name;
}