Lesson 4
Entity Relationships and Fetching Strategies
Introduction

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.

Types of Relationships

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.
Understanding One-to-One Relationship

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 one PASSPORT and each PASSPORT belongs to one PERSON.
  • The ||--|| notation indicates this one-to-one relationship.
  • Both tables contain an ID, and there are foreign keys (passportId in PERSON and personId in PASSPORT) representing the relationship.
Implementing One-to-One Relationship

Let's see how we can define a One-to-One relationship using JPA annotations:

Java
1package 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.

Understanding One-to-Many Relationship

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 multiple TODO_ITEM entries, but each TODO_ITEM belongs to a single PERSON.
  • The ||--o| notation indicates this one-to-many relationship.
  • The PERSON table includes a list of TODO_ITEM IDs to show the relation, while each TODO_ITEM includes a personId representing the PERSON it belongs to.
Implementing One-to-Many Relationship

Here’s how we can define a One-to-Many relationship using JPA annotations:

Java
1package 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.

Fetching Strategies

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.

Understanding Many-to-Many Relationship

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 multiple DOCUMENT entries, and each DOCUMENT can have multiple PERSON collaborators.
  • The ||--o| notation on both sides of the relationship indicates this many-to-many relationship.
  • The intermediary table PERSON_DOCUMENT contains foreign keys personId and documentId representing the relationship.
Implementing Many-to-Many Relationship

Here's how we can define a Many-to-Many relationship using JPA annotations:

Java
1package 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.

Summary

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.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.