
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: April 3, 2025
JPA makes dealing with relational database models from our Java applications less painful. Things are simple when we map every table to a single entity class.
But we sometimes have reasons to model our entities and tables differently:
In this short tutorial, we’ll see how to tackle this last scenario.
Let’s say we run a restaurant, and we want to store data about every meal we serve:
Since there are many possible allergens, we’re going to group this data set together.
Furthermore, we’ll also model this using the following table definitions:
Now let’s see how we can map these tables to entities using standard JPA annotations.
The most obvious solution is to create an entity for both classes.
Let’s start by defining the MealWithMultipleEntities entity:
@Entity
@Table(name = "meal")
public class MealWithMultipleEntities {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Long id;
@Column(name = "name")
String name;
@Column(name = "description")
String description;
@Column(name = "price")
BigDecimal price;
@OneToOne(mappedBy = "meal")
AllergensAsEntity allergens;
// standard getters and setters
}
Next, we’ll add the AllergensAsEntity entity:
@Entity
@Table(name = "allergens")
class AllergensAsEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "meal_id")
Long mealId;
@OneToOne
@PrimaryKeyJoinColumn(name = "meal_id")
Meal meal;
@Column(name = "peanuts")
boolean peanuts;
@Column(name = "celery")
boolean celery;
@Column(name = "sesame_seeds")
boolean sesameSeeds;
// standard getters and setters
}
We can see that meal_id is both the primary key and also the foreign key. That means we need to define the one-to-one relationship column using @PrimaryKeyJoinColumn.
However, this solution has two problems:
One possible resolution to the first problem is to add the @NotNull annotation to the allergens field on our MealWithMultipleEntities entity. JPA won’t let us persist the MealWithMultipleEntities if we have a null AllergensAsEntity.
However, this is not an ideal solution. We want a more restrictive one, where we don’t even have the opportunity to try to persist a MealWithMultipleEntities without AllergensAsEntity.
We can create a single entity specifying that we have columns in different tables using the @SecondaryTable annotation:
@Entity
@Table(name = "meal")
@SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id"))
class MealAsSingleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Long id;
@Column(name = "name")
String name;
@Column(name = "description")
String description;
@Column(name = "price")
BigDecimal price;
@Column(name = "peanuts", table = "allergens")
boolean peanuts;
@Column(name = "celery", table = "allergens")
boolean celery;
@Column(name = "sesame_seeds", table = "allergens")
boolean sesameSeeds;
// standard getters and setters
}
Behind the scenes, JPA joins the primary table with the secondary table and populates the fields. This solution is similar to the @OneToOne relationship, but this way, we can have all of the properties in the same class.
It’s important to note that if we have a column that is in a secondary table, we have to specify it with the table argument of the @Column annotation. If a column is in the primary table, we can omit the table argument since JPA looks for columns in the primary table by default.
Also, note that we can have multiple secondary tables if we embed them in @SecondaryTables. Alternatively, from Java 8, we can mark the entity with multiple @SecondaryTable annotations since it’s a repeatable annotation.
As we’ve seen, @SecondaryTable maps multiple tables to the same entity. We also know that @Embedded and @Embeddable do the opposite and map a single table to multiple classes.
Let’s see what we get when we combine @SecondaryTable with @Embedded and @Embeddable:
@Entity
@Table(name = "meal")
@SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id"))
class MealWithEmbeddedAllergens {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
Long id;
@Column(name = "name")
String name;
@Column(name = "description")
String description;
@Column(name = "price")
BigDecimal price;
@Embedded
AllergensAsEmbeddable allergens;
// standard getters and setters
}
@Embeddable
class AllergensAsEmbeddable {
@Column(name = "peanuts", table = "allergens")
boolean peanuts;
@Column(name = "celery", table = "allergens")
boolean celery;
@Column(name = "sesame_seeds", table = "allergens")
boolean sesameSeeds;
// standard getters and setters
}
It’s a similar approach to what we saw using @OneToOne. However, it has a couple of advantages:
Nevertheless, this one-to-one-like solution works only when the two tables have matching ids.
It’s worth mentioning that if we want to reuse the AllergensAsEmbeddable class, it would be better if we defined the columns of the secondary table in the MealWithEmbeddedAllergens class with @AttributeOverride.
In this short tutorial, we’ve seen how we can map multiple tables to the same entity using the @SecondaryTable JPA annotation.
We also saw the advantages of combining @SecondaryTable with @Embedded and @Embeddable to get a relationship similar to one-to-one.