The labs on this site need JavaScript to run. The same content is written out below.
Learn › JPA › Relationships
The owning side of a relationship
You added a member to the team's list, so why is the foreign key in the database empty?
When a team and a member point to each other, the Java objects have two references (the team of the member, and the members of the team). In the database, however, the only thing that links them is one foreign key, team_id in the member table. JPA looks at only one of the two references when it writes the foreign key. That side is called the owning side of the relationship.
Checked against real execution · All 12 combinations were run on Hibernate 7.4.5 and H2, and the tables, the SQL that was sent, the foreign key values, and the size of the list were checked against the results
Concepts in this lab
- 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.
- Unidirectional and bidirectional relationships
- These words describe the direction in which one entity refers to another entity. If only the member has the team as a field, the relationship is unidirectional. If the team also has a list of members as a field, so that they point to each other, it is bidirectional. In tables, a single foreign key allows a join from either side, so a table has no direction. An object, however, can reach the other one only from the side that holds the reference. So a bidirectional relationship in objects is really two different unidirectional references, and you have to decide which of the two manages the foreign key.
- 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.
- First-level cache
- The store inside the persistence context that keeps entities by identifier. If you load the same identifier again within the same transaction, it returns the stored entity without sending a query to the database. As a result, entities with the same identifier are always the same object within the same transaction. By default it disappears when the transaction ends (in a web request with Open Session in View (OSIV) turned on, it lasts until the request ends), so it is different from an application-wide cache.
- Transactional write-behind and flush
- Transactional write-behind means that INSERT, UPDATE, and DELETE statements are not sent to the database right away. They are collected and sent together. However, for an entity whose primary key is generated with the IDENTITY strategy (such as AUTO_INCREMENT in MySQL), the INSERT runs immediately when you request the save, because the identifier must be obtained. A flush sends the collected changes to the database. It happens automatically just before the transaction commits and just before a JPQL query runs. A flush only sends SQL to the database. It does not commit the transaction.
Try these
- Default settings — You add the member only to the team's list. There is no exception, but team_id in the database is null.
- Setting the member side only — team_id is stored, but the list of the team that is read in the same transaction is empty.
- Setting both sides — The foreign key in the database and the list in memory are both correct.
- Setting the team side only, then reading again from the database — The list had 1 member in memory, but when it is read again from the database, it has 0.
- Setting the member side only, then reading again from the database — The foreign key was stored, so the list that is read again has 1 member.
- Leaving out mappedBy — A separate join table team_member is created, and one more INSERT is sent.
Running with the default settings
- new Team("Engineering"): Creates a team object. It exists only in Java memory so far, and the members list is empty.
- em.persist(team): The primary key is created with the IDENTITY strategy, so the INSERT is sent right away and the id 1 chosen by the database comes back.
- new Member("Alice"): Creates a member object. Its team field is still null.
- team.getMembers().add(member): The member goes into the members list of the team object. But this list has mappedBy = "team". It means "the foreign key is managed by the team field of Member, and I only read", so JPA does not look at this list when it builds SQL.
- em.persist(member): An INSERT is sent right away. The team_id column gets the value that is read from the team field of the member object. The team field is null, so team_id is null too. Adding the member to the list of the team had no effect on this value.
- em.find(Team.class, team.getId()): The first-level cache has the team with id 1, so it returns that object without going to the database. found and team are the same object.
- found.getMembers().size(): found is the team object that was created first, so it returns the number of elements in the Java list as it is. It does not ask the database. A member was put in the list, so the result is 1. In the database, team_id is null, but in memory it looks as if the team has a member.
- Commit: There is no more SQL to send. The member that was added to the list of the team never reached the database. No exception is thrown either.
Result of this run
- If you set the value only on the side that is not the owning side (Team.members, which has mappedBy), the foreign key is not stored. No exception is thrown, so it is easy to think it was saved. Inside the same transaction, the member is visible in the list in memory, so it is even harder to notice.
Code
@Entity
class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
@OneToMany(mappedBy = "team") // not the owning side. It is read-only
List<Member> members = new ArrayList<>();
}
@Entity
class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id") // the owning side. It manages the foreign key team_id
Team team;
}
// Constructors, getters, and setters are omitted
// Inside a transaction
Team team = new Team("Engineering");
em.persist(team);
Member member = new Member("Alice");
team.getMembers().add(member); // set on the side that is not the owning side
em.persist(member);
Team found = em.find(Team.class, team.getId());
int size = found.getMembers().size();
// Transaction commit
Assumptions of this model
This lab covers only the case of saving one new team and one new member. The case of changing the team of a member that is already saved (the case in which an UPDATE is sent) is not covered. The primary key uses the IDENTITY strategy, so the INSERT is sent as soon as persist is called. All 12 combinations 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 kinds of SQL that were sent and the count per table, whether that SQL was sent at persist or just before commit, the value of member.team_id in the database, the number of rows in the join table, and team.getMembers().size(). The names of the join table and its columns (team_member, team_id, members_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. In the SQL log, an alias follows each table name, and question marks stand in the place of values.
Previous lab: Value types and embeddable types · Next lab: Many-to-one, one-to-many, one-to-one, many-to-many