Welcome to this lesson on Entity Relationships in Spring Data JPA. In the previous lessons, we've covered the basics of JPA repositories and entities, using derived queries for simple methods, and creating query methods for more complex queries. In this lesson, we're going to discuss how to work with relationships between entities in Spring Data JPA.
Before diving into implementation, it's essential to understand the different types of relationships in the context of data persistence with Spring Data JPA:
- One-to-One: For example, a person and their passport. Each person has one passport, and each passport belongs to one person.
- One-to-Many: For example, one person can be assigned to many different ToDos, but one ToDo cannot be assigned to multiple people.
- Many-to-One: This is just the previous example but viewed from the opposite direction — many ToDos are assigned to the same person.
- Many-to-Many: For example, Google Docs and collaborators. Each person can work on many docs, and each doc can have many collaborators.
Let’s start with the One-to-One relationship. Here is a diagram to illustrate the One-to-One relationship between a person and their passport.
Explanation:
- PERSON and PASSPORT tables are shown with a one-to-one relationship.
- Each
PERSON
has onePASSPORT
and eachPASSPORT
belongs to onePERSON
. - The
||--||
notation indicates this one-to-one relationship. - Both tables contain an ID, and there are foreign keys (
passportId
inPERSON
andpersonId
inPASSPORT
) representing the relationship.
Let's see how we can define a One-to-One relationship using JPA annotations:
Java1package com.codesignal.entities; 2 3import jakarta.persistence.*; 4 5@Entity 6@Table(name = "person") 7public class Person { 8 9 @Id 10 @GeneratedValue(strategy = GenerationType.IDENTITY) 11 private Long id; 12 13 private String name; 14 15 @OneToOne(mappedBy = "person", cascade = CascadeType.ALL, fetch = FetchType.LAZY) 16 private Passport passport; 17 18 // Constructors, getters, and setters... 19} 20 21@Entity 22@Table(name = "passport") 23public class Passport { 24 25 @Id 26 @GeneratedValue(strategy = GenerationType.IDENTITY) 27 private Long id; 28 29 private String number; 30 31 @OneToOne 32 @JoinColumn(name = "person_id") 33 private Person person; 34 35 // Constructors, getters, and setters... 36}
In the Person
class, the @OneToOne
annotation indicates a one-to-one relationship with the Passport
class. The mappedBy
attribute specifies the field in the Passport
class that owns the relationship. The cascade = CascadeType.ALL
attribute indicates that all JPA-related changes (like persist, merge, remove, etc.) made to the Person
entity should cascade to the associated Passport
entity. The fetch = FetchType.LAZY
attribute indicates that the Passport
entity should be lazily loaded, meaning it will be fetched from the database only when explicitly accessed.
In the Passport
class, the @OneToOne
annotation defines the other end of the relationship with the Person
class. The @JoinColumn
annotation is used to specify the foreign key column (person_id
) in the PASSPORT
table that maps to the primary key of the PERSON
table.
Next, let's explore the One-to-Many relationship. Here is a diagram to illustrate the One-to-Many relationship where many ToDos are assigned to the same person.
Explanation:
- PERSON and TODO_ITEM tables are shown with a one-to-many relationship.
- Each
PERSON
can have multipleTODO_ITEM
entries, but eachTODO_ITEM
belongs to a singlePERSON
. - The
||--o|
notation indicates this one-to-many relationship. - The
PERSON
table includes a list ofTODO_ITEM
IDs to show the relation, while eachTODO_ITEM
includes apersonId
representing thePERSON
it belongs to.
Here’s how we can define a One-to-Many relationship using JPA annotations:
Java1package com.codesignal.entities; 2 3import jakarta.persistence.*; 4import java.util.List; 5 6@Entity 7@Table(name = "person") 8public class Person { 9 10 @Id 11 @GeneratedValue(strategy = GenerationType.IDENTITY) 12 private Long id; 13 14 private String name; 15 16 @OneToMany(mappedBy = "person", fetch = FetchType.LAZY, cascade = CascadeType.ALL) 17 private List<TodoItem> todoItems; 18 19 // Constructors, getters, and setters... 20} 21 22@Entity 23@Table(name = "todo_item") 24public class TodoItem { 25 26 @Id 27 @GeneratedValue(strategy = GenerationType.IDENTITY) 28 private Long id; 29 30 private String title; 31 private boolean isCompleted; 32 33 @ManyToOne(fetch = FetchType.LAZY) 34 @JoinColumn(name = "person_id") 35 private Person person; 36 37 // Constructors, getters, and setters... 38}
In the Person
class, @OneToMany
indicates a one-to-many relationship with the TodoItem
class, using lazy fetching and cascading all operations. In the TodoItem
class, @ManyToOne
defines the other end of the relationship, with a foreign key specified by @JoinColumn
.
While implementing entity relationships, it's crucial to consider how related entities are fetched from the database. In the previous code snippets, you might have noticed the usage of fetch = FetchType.LAZY
. This is an example of a fetching strategy. In JPA, fetching strategies determine how related entities are loaded from the database when their parent entity is retrieved. Fetching strategies can be applied to associations annotated with @OneToOne
, @OneToMany
, @ManyToOne
, and @ManyToMany
.
There are two primary fetching strategies:
-
Eager Fetching: When a parent entity is retrieved, all related entities are loaded simultaneously. This is specified using
fetch = FetchType.EAGER
. This can result in immediate fetching of all the data necessary at the expense of potentially loading a lot of unnecessary data, leading to performance issues. Note that eager fetching is the default behavior for@ManyToOne
and@OneToOne
relationships. -
Lazy Fetching: When a parent entity is retrieved, related entities are not loaded until they are explicitly accessed. This is specified using
fetch = FetchType.LAZY
. This can improve performance by only loading the data that is actually needed at the moment but can lead to additional queries to the database. Lazy fetching is the default behavior for@OneToMany
and@ManyToMany
relationships.
Finally, let's delve into the Many-to-Many relationship. Here is a diagram to illustrate the Many-to-Many relationship between Google Docs and collaborators.
Explanation:
- PERSON and DOCUMENT tables are shown with an intermediary join table called PERSON_DOCUMENT which implements the many-to-many relationship.
- Each
PERSON
can work on multipleDOCUMENT
entries, and eachDOCUMENT
can have multiplePERSON
collaborators. - The
||--o|
notation on both sides of the relationship indicates this many-to-many relationship. - The intermediary table
PERSON_DOCUMENT
contains foreign keyspersonId
anddocumentId
representing the relationship.
Here's how we can define a Many-to-Many relationship using JPA annotations:
Java1package com.codesignal.entities; 2 3import jakarta.persistence.*; 4import java.util.Set; 5 6@Entity 7@Table(name = "person") 8public class Person { 9 10 @Id 11 @GeneratedValue(strategy = GenerationType.IDENTITY) 12 private Long id; 13 14 private String name; 15 16 @ManyToMany(fetch = FetchType.LAZY) 17 @JoinTable( 18 name = "person_document", 19 joinColumns = @JoinColumn(name = "person_id"), 20 inverseJoinColumns = @JoinColumn(name = "document_id")) 21 private Set<Document> documents; 22 23 // Constructors, getters, and setters... 24} 25 26@Entity 27@Table(name = "document") 28public class Document { 29 30 @Id 31 @GeneratedValue(strategy = GenerationType.IDENTITY) 32 private Long id; 33 34 private String title; 35 36 @ManyToMany(mappedBy = "documents", fetch = FetchType.LAZY) 37 private Set<Person> collaborators; 38 39 // Constructors, getters, and setters... 40}
In the Person
class, the @ManyToMany
annotation defines a many-to-many relationship with the Document
class. The fetch = FetchType.LAZY
specifies that the relationship should be lazily loaded. The @JoinTable
annotation is used to specify the join table used for this relationship. The name
attribute of @JoinTable
defines the name of the join table (person_document
), while joinColumns
and inverseJoinColumns
define the foreign key columns that reference the PERSON
and DOCUMENT
tables, respectively.
In the Document
class, @ManyToMany
is used to define the other side of the relationship with the Person
class. The mappedBy
attribute indicates that the documents
field in the Person
class owns the relationship, meaning the Person
class is responsible for persisting the relationship in the database. Here also, the fetch = FetchType.LAZY
attribute ensures lazy loading.
In this lesson, we explored various types of entity relationships in Spring Data JPA, including One-to-One, One-to-Many and Many-to-Many relationships. We illustrated these relationships with diagrams and provided code examples to show how each relationship type can be implemented using appropriate JPA annotations. By understanding these relationships, you can design more complex and efficient database schemas for your Spring Boot applications. In the next section, you will have the opportunity to practice these concepts with hands-on exercises.