The labs on this site need JavaScript to run. The same content is written out below.
Learn › JPA › Relationships
Many-to-one, one-to-many, one-to-one, many-to-many
How do the tables and foreign keys change depending on the kind of relationship?
The relationships between entities include many-to-one, one-to-many, one-to-one, and many-to-many. Which one you choose changes the number of tables that are created, the table that holds the foreign key, and the SQL that is sent when you save.
Checked against real execution · All 14 combinations were run on Hibernate 7.4.5 and H2, and the tables, the foreign key and unique constraints, the SQL that was sent, and the values left were checked against the results
Concepts in this lab
- Many-to-one and one-to-many relationships
- When many members belong to one team, the relationship is a many-to-one (@ManyToOne) seen from the member, and a one-to-many (@OneToMany) seen from the team. In tables, whichever side you look from, the foreign key sits in the table of the "many" side (member). In a many-to-one, the entity that holds the foreign key manages that value directly, so it is saved with a single INSERT. In a unidirectional one-to-many, the list in the entity on the other side manages the foreign key, so after the rows are inserted, an additional UPDATE that fills in the foreign key is sent.
- One-to-one relationship
- This is a relationship in which an entity is connected to only one other entity, and it is mapped with @OneToOne. The foreign key can be placed in either of the two tables, and the entity of the table that holds the foreign key becomes the owning side of the relationship. A unique constraint is placed on the foreign key column as well, so two rows cannot point to the same counterpart. The table shape is the same as in a many-to-one, and the only difference is this unique constraint.
- Many-to-many relationships and join tables
- A many-to-many is a relationship in which each side has several counterparts. For example, a member can pick several products, and a product can be picked by several members. It cannot be expressed with the foreign keys of two tables alone, so you put in the middle a join table that holds the primary keys of both sides as foreign keys. If you use @ManyToMany, JPA creates and manages this join table automatically. A join table created automatically holds only two foreign keys, so if you need more values such as a quantity or a date, make the join table an entity and solve it with a one-to-many and a many-to-one.
- Owning side and mappedBy
- In a bidirectional relationship, the side that actually manages the foreign key is called the owning side. JPA inserts or updates the foreign key by looking only at the value in the reference on the owning side. On the other side, which is not the owning side (the inverse side), you write the name of the field on the owning side in mappedBy to mark it as read-only, and values you put there are not reflected in the database. The owning side is the entity that corresponds to the table that holds the foreign key, that is, the "many" side of a many-to-one relationship. (The JPA specification says that the "many" side of a bidirectional one-to-many or many-to-one relationship must be the owning side, which is why @ManyToOne has no mappedBy.) If you leave out mappedBy in a bidirectional relationship, the two references are treated as different relationships, and an unintended join table is created.
- Primary key, unique key, foreign key
- A primary key (PK) is the column that uniquely identifies each row of a table. Its values cannot be duplicated or empty, and a table has only one. A unique key is a unique constraint, which prevents duplicate values, and a table can have several. A foreign key (FK) is a column that references the primary key (or a unique key) of another table. It guarantees that the referenced row really exists.
Try these
- Default settings — Unidirectional one-to-many. After the INSERTs, as many UPDATEs as there are members are sent to fill in the foreign key.
- Many-to-one — The tables are the same, but INSERT alone is enough.
- Removing @JoinColumn from the one-to-many — A join table is created instead of a foreign key.
- Many-to-many — The join table member_product is created automatically in the middle.
- One-to-one: foreign key in the member table — A foreign key and a unique constraint are created on member.locker_id.
- One-to-one: foreign key in the locker table — The foreign key moves to locker.member_id.
- One-to-many with 3 members — The UPDATEs increase to 3 as well.
Running with the default settings
- em.persist(team): Saves the team. The primary key uses the IDENTITY strategy, so the INSERT is sent right away and the id 1 comes back.
- Iteration #1, em.persist(member): The Member class has no field that points to the team. So team_id is not in the INSERT statement, and team_id of this row is left as null.
- Iteration #1, team.getMembers().add(member): Adds the member to the list of the team. The foreign key team_id is in the member table, but what manages it is this list in Team. For now, only the list in memory has changed.
- Iteration #2, em.persist(member): The Member class has no field that points to the team. So team_id is not in the INSERT statement, and team_id of this row is left as null.
- Iteration #2, team.getMembers().add(member): Adds the member to the list of the team. The foreign key team_id is in the member table, but what manages it is this list in Team. For now, only the list in memory has changed.
- Transaction commit: A flush happens just before commit. When JPA looks at the members list of the team, it holds 2 members. It now has to fill in the team_id that could not be put in at INSERT time.
- The UPDATE that fills in the foreign key of user1 is sent. team_id changes from null to 1. This is SQL that would not have been sent with many-to-one.
- The UPDATE that fills in the foreign key of user2 is sent. team_id changes from null to 1. This is SQL that would not have been sent with many-to-one.
Result of this run
- In a unidirectional one-to-many, the foreign key is in the member table too. This is because in a table, the foreign key always sits on the "many" side. But what manages it is the list in the entity on the other side, Team. When Member is INSERTed, it cannot know the team, so the foreign key is filled in later with an UPDATE. One more UPDATE was sent for each member (2 in all).
- The table shape is exactly the same as in a many-to-one, but more SQL is sent. So a common approach is to use many-to-one first, and if the team needs a list of its members, to make the relationship bidirectional by adding mappedBy.
Code
@Entity
class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
@OneToMany // one team has many members
@JoinColumn(name = "team_id") // the foreign key is in the other table (member)
List<Member> members = new ArrayList<>();
}
@Entity
class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
} // there is no field that points to the team
// Constructors, getters, and setters are omitted
// Inside a transaction
Team team = new Team("Engineering");
em.persist(team);
for (int i = 1; i <= 2; i++) {
Member member = new Member("user" + i);
em.persist(member);
team.getMembers().add(member);
}
// Transaction commit
Assumptions of this model
Each entity has only the fields id and name, and the primary key uses the IDENTITY strategy, so the INSERT is sent as soon as persist is called. The one-to-many and many-to-many relationships were made unidirectional, in which only one side points to the other. All 14 combinations (many-to-one, one-to-many, one-to-many without @JoinColumn, and many-to-many, each with 1, 2, or 3 counterparts, and one-to-one with two positions of the foreign key) were run on Hibernate 7.4.5 and an H2 database with code of the same structure, and the following were checked to match the screen: the tables and columns that were created, the columns that have a foreign key constraint and the unique constraints, the kinds of SQL that were sent and the count per table, the SQL that was sent before commit, and the values left in the foreign key columns. It was also confirmed from the real SQL that team_id is not in the INSERT statement of a unidirectional one-to-many. The names of the join tables and their columns (team_member, members_id, member_product, products_id) were chosen by Hibernate 7.4.5 by its default rules, and they can be changed with @JoinTable. The SQL statements on the screen are shortened to be easy to read, so they are not letter for letter the same as the real statements. The order of the columns in the table may also differ from the real one.
Previous lab: The owning side of a relationship · Next lab: Inheritance mapping