Fetching entities with complex relationships can consume a lot of resources during calls to your API. To minimize the size of the requested json
string, you can use the LAZY
fetch type. This will result in accessing only the serialized object and its properties – without considering its relations. But often you need to obtain the identifiers of the associated objects as well. Check out how to fetch an entity with an id of a related object while preserving proper serialization and deserialization.
What we are going to build
The example Task
class has id
and name
properties and requires Many-to-One
relation with the Project
entity. We want the json
representation of a Task
instance to look like this:
1 2 3 4 5 |
{ "id": 1, "name": "task_name", "projectId": 1 } |
You can achieve it by using @JsonIdentityInfo
annotation alongside with @JsonIdentityReference
. However, it may spawn the following error when the data is deserialized back to an object: "Unresolved forward references for related entity"
.
Let’s divide our venture into the following stages.
Requirements
The following fragment of the pom.xml
file shows dependencies used in the project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// pom.xml … <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> … |
Define the relation between two entities
Let’s define the Many-to-One
relation to the Project
entity. When our API is requested for task
data, we want to avoid fetching a related project
with all other tasks associated with it. So we specify the fetch type to LAZY
. You can see the relation between task
and project
entities defined in the following code snippet from the Task
entity:
1 2 3 4 5 6 7 |
// Task.java … @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "project_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) private Project project; … |
The @OnDelete annotation allows removing all child records (tasks) whenever the parent record (project) is deleted.
Include the id property from the related entity when fetching the object
If you need a serialized instance of a task containing an identifier of a related project, add @JsonIdentityInfo
annotation when defining the relation:
1 2 3 4 5 |
// Task.java … @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") private Project project; … |
Enforce use of the related object id
To ensure that the project id
will be properly included as a task property during serialization, add the @JsonIdentityReference annotation:
1 2 3 4 5 |
// Task.java … @JsonIdentityReference(alwaysAsId = true) private Project project; … |
Set alwaysAsId
option to true in order to serialize all referenced values as ids. In the annotation documentation you can read that:
“(…) if value of ‘true’ is used, deserialization may require additional contextual information, (…) the default handling may not be sufficient.”
Fix the deserialization issue
To help deserialize the task
object properly, add the @JsonProperty annotation and specify the json
object field name – "projectId"
:
1 2 3 4 5 |
// Task.java … @JsonProperty("projectId") private Project project; … |
Furthermore, we need the setter for the project
property:
1 2 3 4 5 6 7 |
// Task.java … @JsonProperty("projectId") public void setProjectById(Long projectId) { project = Project.fromId(projectId); } … |
We are using a static factory method, as described in Effective Java (3rd Edition) by Joshua Bloch. To obtain a new Project
instance with only id
specified, add the following method to the Project
entity:
1 2 3 4 5 6 7 8 |
// Project.java … public static Project fromId(Long projectId) { Project project = new Project(); project.id = projectId; return project; } … |
After deserialization, the project
property of the task
object will hold a Project
instance in which only the id
property is set to an actual value.
Verify the results
The task
instance was fully serialized and the related project
was passed as a reference value and included in the json
representation of the task
as its property. To ascertain this, let’s add to the project the following test – getsTaskById()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// awesomeproject/backend/src/test/java/in/keepgrowing/awesomeproject/tasks/TaskControllerTest.java package in.keepgrowing.awesomeproject.tasks; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import java.util.Optional; import static org.hamcrest.Matchers.is; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) @WebMvcTest(TaskController.class) @EnableSpringDataWebSupport public class TaskControllerTest { @MockBean private TaskService taskService; @Autowired private MockMvc mvc; @Test public void getsTaskById() throws Exception { Task task = new Task("test_task"); task.setProjectFromId(1L); given(taskService.getTaskById(1L)).willReturn(Optional.of(task)); mvc.perform(get("/api/tasks/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.name", is(task.getName()))) .andExpect(jsonPath("$.projectId").value(is(task.getProject().getId()), Long.class)); } } |
The source code for the Task entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// Task.js package in.keepgrowing.awesomeproject.tasks; import com.fasterxml.jackson.annotation.*; import in.keepgrowing.awesomeproject.projects.Project; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import javax.persistence.*; import javax.validation.constraints.NotBlank; import java.util.Objects; @Entity public class Task { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "Provide a task name") private String name; @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "project_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") @JsonIdentityReference(alwaysAsId = true) @JsonProperty("projectId") private Project project; public Task() { } @JsonCreator public Task(@NotBlank(message = "Provide a task name") String name) { this.name = name; } @JsonProperty("projectId") public void setProjectById(Long projectId) { project = Project.fromId(projectId); } … // the rest of getters and setters omitted for the sake of clarity } |
Photo by Toni Cuenca on StockSnap
Nice Post, thank you.
2.5.0.0
Nice blog but I think
setProjectFromId()
method in the integration test should be renamed tosetProjectById()
as declared in the Task class.I didn’t get really why we need to add the setter for projectId and why we must specify
projectId
in the JsonProperty annotation.thank you!
I aws going crazy with “com.fasterxml.jackson.databind.deser.UnresolvedForwardReference: Unresolved forward references for ” exception
That help me me a lot!
Very nice post which helped me a lot, however once versions are updated, to me it didn’t work. Using jackson 2.11.1, I needed to do quite different which on this example would be:
In the Project class having the annotation:
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = Project.class)
in the class levelMy Task class became also a little different:
@ManyToOne(fetch = FetchType.EAGER, optional = false) //fetch must be eager here, however in scenarios where this cannot happen, need to solve the problem related to the lazy initialization of the relationship.
@JsonProperty("projectId")
private Project project;
After this post I realized that also what I tried to use before didn’t work the way I was expecting, but at least that shed some light to a very simple solution once what I wanted mostly was to avoid sending the whole instance of the relationship as part of a response (JSON) from an http request.
Just used getters and setters to return only the desired property of the related entity and used @JsonIgnore from Jackson to avoid sending the whole entity of the relationship and created getters and setters to the specific property of the relationship that I want to deal with.
Oh my god this is exactly what I was looking for. Thank you so much!