Handling Circular References in Java Spring Boot

Dhrubok Infotech Services
4 min readJun 14, 2020

--

Spring Boot is undoubtedly one of the finest frameworks for creating enterprise-grade web applications. It reduces lots of development time and increases productivity. It feels like magic when a lot of boilerplate code can be replaced by just one annotation. Finally, the community of java is super-enriched with a lot of resources. However, Almost all Java projects that store information in a relational database use JPA with Hibernate or EclipseLink as its most popular implementations. Sometimes in large projects, it can be difficult to maintain references for other classes in the entity class. Today I will discuss one of the challenges I faced when working on this

1. The problem

Let me first define circular dependency in OOP terms.

Suppose, I have a class called A which has class B’s Object. (in UML terms A HAS B). at the same time, we have another class B which is also composed of Object of class A (in UML terms B HAS A). This creates the circular dependency because while creating the object of A, Jackson must serialize or deserialize. On the other hand, while creating an object of B, Jackson must serialize/deserialize again. This is something like egg vs. chicken problem.

– Talk is cheap, Show me the code!
-Okay, Sure!

@Entity
@Table(name = AppTables.employee)
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = AppTables.employeeTable.id)
private long id;
@Column(name = AppTables.employeeTable.name)
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = AppTables.employeeTable.department)
private Department department;
// Getters and setters are omitted for brevity
}

This is the Employee model class. Employees are assigned to a department. Joining the department is done with ManyToOne annotation. Now, let us take a look at our Department class.

@Entity
@Table(name = AppTables.department)
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = AppTables.departmentTable.id)
private Long id;
@Column(name = AppTables.departmentTable.name)
private String name;
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = AppTables.departmentTable.departmentHead)
private Employee departmentHead;
// Getters and setters are omitted for brevity
}

Look at the department class closely. You see, not only Employee class have Department but also Department has one department head (which is an employee). Can you guess where the problem is?

The problem occurs when we want to fetch an employee who is the department head of his own department. Not only it will create a cycle but also it will cause an infinite loop to be returned.

Part of the error log is shown below.

java.lang.StackOverflowError: null
at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_241]
at java.lang.ClassLoader.defineClass(Unknown Source) ~[na:1.8.0_241]
at java.security.SecureClassLoader.defineClass(Unknown Source) ~[na:1.8.0_241]
at java.net.URLClassLoader.defineClass(Unknown Source) ~[na:1.8.0_241]
at java.net.URLClassLoader.access$100(Unknown Source) ~[na:1.8.0_241]
at java.net.URLClassLoader$1.run(Unknown Source) ~[na:1.8.0_241]
at java.net.URLClassLoader$1.run(Unknown Source) ~[na:1.8.0_241]
at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_241]
at java.net.URLClassLoader.findClass(Unknown Source) ~[na:1.8.0_241]
at java.lang.ClassLoader.loadClass(Unknown Source) ~[na:1.8.0_241]
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source) ~[na:1.8.0_241]
at java.lang.ClassLoader.loadClass(Unknown Source) ~[na:1.8.0_241]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:740) ~[jackson-databind-2.10.2.jar:2.10.2]

I tried to solve this problem with the following code in Employee class.

public Department getDepartment() {
if (this.department != null) {
if (this.department.getDepartmentHead() != null) {
this.department.setDepartmentHead(null)
}
}
return department;
}

What I tried to achieve is, when employees are retrieved department of the department head of that particular employee will be excluded. Thus there will be no circular references.

-That’s it. Right?
-Umm, Not really.

You should not mess with default getters and setters. The reason is Jackson. Jackson by default reuses objects during serialization. So, When I try to retrieve departments. Once a department head is set to null, Always the department head returns as null. Which is not expected behavior.

2. The Bad Solution

Jackson provides @JsonIdentityInfo in order to solve this problem. @JsonIdentityInfo is used to handle circular reference of an object by serializing the back-reference’s identifier rather than serializing the complete reference.@JsonIdentityInfo allows serializing a POJO by id when it is encountered second time during serialization.

@Entity
@Table(name = AppTables.department)
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Department {
//.....
//.....
}

Which solves the problem of circular referencing and infinite loop. But, deserializing it on the client-side is not feasible. Even sometimes it might not even be possible to deserialize this json without writing up custom deserializer.

3. The Good Solution

@JsonIgnoreProperties annotation from Jackson comes to rescue here. In Employee class this following code just before department attribute will do the job for us.

@JsonIgnoreProperties(ignoreUnknown = true, value = {"department"})
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = AppTables.departmentTable.departmentHead) private Employee departmentHead;

It will do exactly what needs to be done. It will ignore department head when the department of an employee is deserialized. Similarly, in Department class, we can add the annotation just before departmentHead to ignore department of the department head.

@JsonIgnoreProperties(ignoreUnknown = true, value = {"department"})
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = AppTables.departmentTable.departmentHead)
private Employee departmentHead;

Not only this solves the deserialization problem but also it gives us the control to ignore one or more attribute to ignore during serialization.

That’s it for today. Thank you for reading. See you around. If you have any questions, you can email at info@dhrubokinfotech.com.

The original post can be found here.

--

--

Dhrubok Infotech Services
Dhrubok Infotech Services

Written by Dhrubok Infotech Services

Supercharge your business with tailor-made software solutions.

No responses yet