Spring Data JPA is a set of standards that defines how Java objects are represented in a database. JPA provides a set of annotations and interfaces that make it possible to configure and map Java objects to relational database tables. Relationships between Java objects are provided through annotations (one-to-one, one-to-many, many-to-one, many-to-many).
The implementations of the JPA specifications are provided by the object-relational mapping tools (ORM) like Hibernate. JPA makes it easier to switch from one ORM tool to another without refactoring code since it abstracts the complexities involved with various ORM tools.
JPA falls between the ORM and the application layer. In this tutorial, we will be modeling a Recipe
application using Spring Data and JPA. The entity-relationship diagram for our application is shown below.
Prerequisite
Before we begin we will need the following:
- JDK installed on your computer.
- Favourite IDE.
- Some knowledge of Java and Spring Boot.
Creating the application
We will be using spring initializr to create our application.
- Open Spring initializr in your browser.
- Select the Kotlin language.
- Add
Spring Web
,Spring Data JPA
, andH2 Database
dependencies. - Leave other configurations as default and click on generate the project.
- Unzip the downloaded project and open it in your favorite IDE. I will be using Intelij IDEA community which is available for free.
- Sync the project with maven to download and all the dependencies.
Domain
The domain package is where we will define our models.
- In the root package where the
DemoApplication.kt
file exists, create a new package with the namedomain
. - In the
domain
package you created above, create two Kotlin files with the namesRecipe.kt
andIngredient.kt
.
JPA mappings
There are two types of JPA mappings:
- Unidirectional mapping - This is where the JPA mapping is only done on one side of the relationship. If entity A has a one-to-many relationship with entity B then only a one-to-many relationship annotation is on entity A.
- Bidirectional mapping - This is where the JPA mappings are declared on both entities that are related. If entity A has a one-to-many relation with entity B then a one-to-many annotation is used on entity A and a Many-To-One annotation is used on entity B. This type of mapping is recommended since it makes it possible to navigate the object graph in both directions.
JPA CASCADE types
JPA CASCADE types control how state changes are cascaded from the parent object to child objects.
- PERSIST - save operations are cascaded to related entities.
- MERGE - related entities are merged if the owning entity is merged.
- REFRESH - related entities are refreshed when the owning entity is refreshed.
- REMOVE - removes all the related entities whenever the owning entity is deleted.
- DETACH - detaches all the related entities if a manual detach occurs.
- ALL - applies all the above cascade options.
JPA relationships
- OneToMany Relation
In this type of JPA relation, a row in the parent entity is referenced by many child records in another entity. From our entity relationship diagram above we can see that the
Recipe
entity has a OneToMany relationship with the ingredient entity meaning that a single recipe is capable of having several ingredients.
In the Recipe.kt
file we created earlier, add the code snippets below.
import javax.persistence.*
@Entity
data class Recipe(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //Uses underlying persistence framework to generate an Id
var id: Long?,
var description: String?,
var prepTime: String?,
var cookTime: String?,
var servings: String?,
var url: String?,
var directions: String?,
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
var ingredient: Set<Ingredient>?
)
@Entity
annotation marks theRecipe
data class as a JPA entity that can be persisted into the database.@Id
annotation marks theid
field as the primary for the database table that will be generated from theRecipe
entity.@GeneratedValue(strategy = GenerationType.IDENTITY)
annotation sets theid
field to be autogenerated andGenerationType.IDENTITY
marks the field as unique.@OneToMany
annotation creates an OneToMany relationship betweenRecipe
entity and theIngredient
entity.mappedBy = "recipe"
indicates that therecipe
field in theIngredient
entity is the foreign key for theRecipe
entity.
In the Ingredient.kt
file, add the snippet below.
@Entity
data class Ingredient(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?,
val description: String?,
val amount: BigDecimal?,
@ManyToOne
val recipe: Recipe
)
@ManyToOne
annotation creates a bidirectional mapping that makes it possible to navigate the object graph to and fromIngredient
andRecipe
.
- OneToOne Relation
In this type of JPA relation, an entity can only belong to another entity. In our
Recipe
entity add thenotes
variable of the typeNote
that we are going to create.
@Entity
data class Recipe(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //Uses underlying persistence framework to generate an Id
var id: Long?,
var description: String?,
var prepTime: String?,
var cookTime: String?,
var servings: String?,
var url: String?,
var directions: String?,
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
var ingredient: Set<Ingredient>?,
@OneToOne(cascade = [CascadeType.ALL])
var notes: Notes?,//Foreign Key
)
@OneToOne
annotation indicates that theNotes
entity will have a One-to-one relationship with theRecipe
entity.
In the domain
package, create a Kotlin file with the name Notes.kt
. In the Notes.kt
file created, add the code snippet below.
@Entity
data class Notes(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long?,
@OneToOne
var recipe: Recipe?,
@Lob //Allows for more than 256 characters in the notes field as hibernate always limits the String field to 256 characters.
var notes: String?
)
@OneToOne
annotation creates a bidirectional mapping with theRecipe
entity.
- ManyToMany relationship In this type of JPA relationship, one or more rows from an entity are associated with one or more rows from another entity.
From our entity relation diagram, we see that the Recipe
entity has a ManyToMany relation with the Category
entity, meaning a recipe can belong to many categories and vice versa.
In the domain
package create a Kotlin file with the name Category.kt
. In the Category.kt
file add the code snippet below.
@Entity
data class Category(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long,
val name: String,
@ManyToMany(mappedBy = "category")
val recipe: Set<Recipe>
)
In the Recipe
entity, add the category
field and annotate it with the @ManyToMany
annotation.
@Entity
data class Recipe(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //Uses underlying persistence framework to generate an Id
var id: Long?,
var description: String?,
var prepTime: String?,
var cookTime: String?,
var servings: String?,
var url: String?,
var directions: String?,
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
var ingredient: Set<Ingredient>?,
@OneToOne(cascade = [CascadeType.ALL])
var notes: Notes?,//Foreign Key
@ManyToMany
@JoinTable(
name = "recipe_category",
joinColumns = [JoinColumn(name = "recipe_id")],
inverseJoinColumns = [JoinColumn(name = "category_id")]
)
val category: Set<Category>?
)
- The
@JoinTable
annotation generates a table with the namerecipe_category
that will store the primary keys for bothRecipe
andCategory
. The generated table has two columns;recipe_id
that references the id in theRecipe
table, andcategory_id
which references the id column of theCategory
table.
- Enumerated Used to store map enum values to database representation in JPA.
In the domain
package create a Kotlin enum class with the name Difficulty
. Add the code snippet below into the enum class created above.
enum class Difficulty {
EASY, MODERATE, HARD
}
In the Recipe
entity add the difficulty
field of the type Difficulty as shown below.
@Entity
data class Recipe(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //Uses underlying persistence framework to generate an Id
var id: Long?,
var description: String?,
var prepTime: String?,
var cookTime: String?,
var servings: String?,
var url: String?,
var directions: String?,
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
var ingredient: Set<Ingredient>?,
@OneToOne(cascade = [CascadeType.ALL])
var notes: Notes?,//Foreign Key
@ManyToMany
@JoinTable(
name = "recipe_category",
joinColumns = [JoinColumn(name = "recipe_id")],
inverseJoinColumns = [JoinColumn(name = "category_id")]
)
val category: Set<Category>?,
@Enumerated(value = EnumType.STRING)
val difficulty: Difficulty,
)
- The
@Enumerated(value = EnumType.STRING)
sets the difficulty field to enumeration. There are two enum types;EnumType.STRING
andEnumType.ORDINAL
. EnumType.ORDINAL
stores the enum values as integers i.e.EASY
as 1,HARD
as 3 whileEnumType.STRING
stores the values as string i.e.EASY
as EASY.
Conclusion
Now that you have learned how to model the database using Spring Data JPA, implement the JPA repositories, and then create a REST controller for our recipe application. Source code for the application can be found here.
Happy coding!
Peer Review Contributions by: Peter Kayere