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.
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:
- The 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 in Kotlin:
Kotlin1package 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.
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 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 in Kotlin:
Kotlin1package 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 ?
.
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
.
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 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 in Kotlin:
Kotlin1package 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.
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.