Lesson 4
Entity Relationships in Spring Data JPA Using Kotlin
Introduction

Welcome to this lesson on Entity Relationships in Spring Data JPA using Kotlin. 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:

  • The 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 in Kotlin:

Kotlin
1package com.codesignal.entities 2 3import jakarta.persistence.* 4import kotlin.jvm.Transient 5 6@Entity 7@Table(name = "person") 8data class Person( 9 @Id 10 @GeneratedValue(strategy = GenerationType.IDENTITY) 11 val id: Long? = null, 12 13 val name: String, 14 15 @OneToOne(mappedBy = "person", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) 16 @Transient 17 var passport: Passport? = null 18) 19 20@Entity 21@Table(name = "passport") 22data class Passport( 23 @Id 24 @GeneratedValue(strategy = GenerationType.IDENTITY) 25 val id: Long? = null, 26 27 val number: String, 28 29 @OneToOne 30 @JoinColumn(name = "person_id") 31 var person: Person? = null 32)

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.

The @Transient annotation is used in the Person class to indicate that the passport property should not be persisted to the database. This means that the passport field will be ignored by the JPA provider and will not be mapped to any database column. It is used here because the passport field already has a relationship mapping and doesn't need its own separate database column.

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:

  • The 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 in Kotlin:

Kotlin
1package com.codesignal.entities 2 3import jakarta.persistence.* 4 5@Entity 6@Table(name = "person") 7data class Person( 8 @Id 9 @GeneratedValue(strategy = GenerationType.IDENTITY) 10 val id: Long? = null, 11 12 val name: String, 13 14 @OneToMany(mappedBy = "person", fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) 15 var todoItems: List<TodoItem> = mutableListOf() 16) 17 18@Entity 19@Table(name = "todo_item") 20data class TodoItem( 21 @Id 22 @GeneratedValue(strategy = GenerationType.IDENTITY) 23 val id: Long? = null, 24 25 val title: String, 26 val isCompleted: Boolean, 27 28 @ManyToOne(fetch = FetchType.LAZY) 29 @JoinColumn(name = "person_id") 30 var person: Person? = null 31)

In this Kotlin code, the Person class manages a list of TodoItem entities. The use of mutableListOf() ensures that the collection is mutable. Properties are by default non-nullable unless specified as nullable with ?.

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.

There are two primary fetching strategies:

  • Eager Fetching: All related entities are loaded simultaneously with the parent. It's specified using fetch = FetchType.EAGER.
  • Lazy Fetching: Related entities are loaded only when explicitly accessed, specified using fetch = FetchType.LAZY.
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:

  • The 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 in Kotlin:

Kotlin
1package com.codesignal.entities 2 3import jakarta.persistence.* 4 5@Entity 6@Table(name = "person") 7data class Person( 8 @Id 9 @GeneratedValue(strategy = GenerationType.IDENTITY) 10 val id: Long? = null, 11 12 val name: String, 13 14 @ManyToMany(fetch = FetchType.LAZY) 15 @JoinTable( 16 name = "person_document", 17 joinColumns = [JoinColumn(name = "person_id")], 18 inverseJoinColumns = [JoinColumn(name = "document_id")] 19 ) 20 var documents: Set<Document> = mutableSetOf() 21) 22 23@Entity 24@Table(name = "document") 25data class Document( 26 @Id 27 @GeneratedValue(strategy = GenerationType.IDENTITY) 28 val id: Long? = null, 29 30 val title: String, 31 32 @ManyToMany(mappedBy = "documents", fetch = FetchType.LAZY) 33 var collaborators: Set<Person> = mutableSetOf() 34)

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 using Kotlin, including One-to-One, One-to-Many, and Many-to-Many relationships. We illustrated these relationships with diagrams and provided Kotlin 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.