Spring Boot | Spring Data JPA
Understanding Relationships in JPA: One-to-Many with Composite Primary Key
Discussing “One-to-Many with Composite Primary Key” and examining how JPA/Hibernate generates the corresponding tables
As mentioned in the introductory article of this series, I’ve faced difficulties in implementing JPA relationships correctly in my applications.
To enhance my understanding of them, I developed a GitHub repository called ivangfr/spring-data-jpa-relationships, containing simple examples for each relationship type.
In this series of articles, we will demonstrate each JPA relationship type by presenting the necessary code to map the entities and examining how JPA/Hibernate generates the tables.
I hope you find these articles and the GitHub repository helpful.
Today, we will talk about “One-to-Many with Composite Primary Key”. So, let’s get started!
One-to-Many with Composite Primary Key
In this example, we will associate Player and Weapon entities. A player can have zero, one, or more weapons, and a weapon can only belong to one player. Additionally, we intend to utilize the Player’s ID as the primary key for the Weapon entity.
Below is the desired database model we aim to achieve.

This is the complete and final code. Next, we will explain it in detail.
@Entity
@Table(name = "players")
public class Player {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@OneToMany(mappedBy = "player", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Weapon> weapons = new LinkedHashSet<>();
@Column(nullable = false)
private String name;
// getters and setters
}
@Entity
@Table(name = "weapons")
@IdClass(WeaponPk.class)
public class Weapon {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "player_id")
private Player player;
@Column(nullable = false)
private String name;
// getters and setters
}
public class WeaponPk implements Serializable {
private Long id;
private Long player;
// getters and setters
}We create three classes, Player, Weapon, and WeaponPk. We use the @Entity annotation in Player and Weapon to indicate that these classes correspond to database tables. The @IdClass(WeaponPk.class) annotation is used to indicate that the Weapon entity uses a composite primary key represented by the WeaponPk class.
By default, the table name would be the same as the entity name, but we want them to be in plural form. We can achieve this by using the @Table annotation, where we specify the desired table name. For the Player entity, we use “players”, and for the Weapon entity, we use “weapons”.
Next, we add the id field of type Long and the name field of type String to both Player and Weapon classes.
To ensure that the name field in the Player and Weapon entities cannot have null values, we add the @Column(nullable = false) annotation to it. This annotation tells JPA that the corresponding database column should be defined as NOT NULL.
For theid field in both Player and Weapon entities, we add the annotations: @Id and @GeneratedValue(strategy = GenerationType.SEQUENCE). The @Id annotation indicates that the field is the primary key for the entity, while the @GeneratedValue annotation specifies the strategy for generating the ID.
In the Player class, we create a weapons field that contains a Set of Weapon objects. In the Weapon class, we create a player field that contains a Player object. The player field, together with the id field, forms the Weapon’s class composite primary key.
To establish this one-to-many/many-to-one relationship, we use the @OneToMany annotation on the weapons field in the Player class and the @ManyToOne annotation on the player field in the Weapon class.
The mappedBy used in the @OneToMany annotation on the weapons field in the Player class indicates that the relationship between Player and Weapons is bidirectional, and that the owning side of the relationship is the Weapon entity. The mappedBy attribute specifies the name of the field in the Weapon class that owns the relationship, which in this case is the player field.
The cascade = CascadeType.ALL is used to specify that all cascading operations should be applied to the Weapon entity when the Player entity is persisted, updated, or deleted. This means that if a Player is deleted, the associated Weapon will also be deleted.
The orphanRemoval = true means that when a weapon is removed from the Set of weapons in the Player entity, it will also be deleted from the database.
The fetch = FetchType.LAZY parameter, specified in the @ManyToOne annotation of the Weapon class, indicates that the associated player should be loaded lazily only when necessary to enhance performance.
We use the @JoinColumn annotation on the player field in the Weapon class to specify that this field is the join column for the relationship between the Weapon entity and the Player entity. The name attribute in the @JoinColumn annotation specifies the name of the foreign key column in the Weapon table. In our example, the foreign key column’s name is player_id.
Lastly, let’s describe the tables in PostgreSQL terminal.
jparelationshipsdb=# \d players
Table "public.players"
Column | Type | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
id | bigint | | not null |
name | character varying(255) | | not null |
Indexes:
"players_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "weapons" CONSTRAINT "fk1sex5dl81khms5e1rggdufkbj" FOREIGN KEY (player_id) REFERENCES players(id)
jparelationshipsdb=# \d weapons
Table "public.weapons"
Column | Type | Collation | Nullable | Default
-----------+------------------------+-----------+----------+---------
id | bigint | | not null |
player_id | bigint | | not null |
name | character varying(255) | | not null |
Indexes:
"weapons_pkey" PRIMARY KEY, btree (id, player_id)
Foreign-key constraints:
"fk1sex5dl81khms5e1rggdufkbj" FOREIGN KEY (player_id) REFERENCES players(id)The players table has the id column as Primary Key. The weapons table’s Primary Key is formed by two columns, id and player_id, where id is an auto increment column and the player_id column is the Foreign Key for the id column in the players table.
Conclusion
In this series of articles, we are exploring the four different types of relationships in JPA: one-to-one, one-to-many / many-to-one, and many-to-many. By presenting simple examples for each type of relationship and providing necessary code to map the entities, we hope to help developers to gain a better understanding of the JPA relationships.
Support and Engagement
If you enjoyed this article and would like to show your support, please consider taking the following actions:
- 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
- 🌐 Share my story on Social Media;
- 🔔 Follow me on: Medium | LinkedIn | Twitter | GitHub;
- ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.





