Issue
Following the excellent advice given in this post...
How to model a three-way relationship in a JPA Spring Boot micro service with a MySQL back end
..I have coded three Entity classes (shown below) in a Spring-boot Java application to represent the skills base of my organisation, where each user may have many skills. A 'Level' enum attribute is used to represent competence of the user for each particular skill.
I added some Controller mappings, started my application and did some basic API testing, in which I was able to successfully add skills to users and specify the additional level attribute.
Here are the Entity classes:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Integer id;
private String name;
private String email;
@OneToMany(mappedBy = "user",
cascade = CascadeType.MERGE,
orphanRemoval = true
)
private Set<UserSkill> skills = new HashSet<>();
(getters and setters)
@Entity
@Table(name = "skills")
public class Skill {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Integer id;
private String name;
@OneToMany(mappedBy = "skill",
cascade = CascadeType.MERGE,
orphanRemoval = true
)
private Set<UserSkill> users = new HashSet<>();
(getters and setters)
@Entity
@Table(name = "users_skills")
public class UserSkill {
@EmbeddedId
private UserSkillId id;
@ManyToOne
@JoinColumn(name = "fk_user", insertable = false, updatable = false)
private User user;
@ManyToOne
@JoinColumn(name = "fk_skill", insertable = false, updatable = false)
private Skill skill;
@Column
@Enumerated(EnumType.STRING)
private Level level;
public UserSkill() {
}
public UserSkill(User u, Skill s, Level l) {
// create primary key
this.id = new UserSkillId(u.getId(), s.getId());
// initialize attributes
this.user = u;
this.skill = s;
this.level = l;
// update relationships to assure referential integrity
u.getSkills().add(this);
s.getUsers().add(this);
}
I am assigning skills to users using Repository classes representing the three Entities:
User user = userRepository.findById(userid).get();
Skill skill= skillRepository.findById(skillid).get();
Level level = Level.valueOf(levelid);
UserSkill userSkill = new UserSkill(user, skill, level);
userSkillRepository.save(userSkill);
In my Controller, I have a mapping to retrieve the user and associated skills and add this to my Model:
@GetMapping("/user/{userid}/skills/get")
public String getUserSkills(@PathVariable("userid") Integer userid, Model model) {
User user = userRepository.findById(userid).get();
model.addAttribute("user", user);
Set<UserSkill> userSkills = userSkillRepository.findAllByUser(user);
model.addAttribute("userskills", userSkills);
return "update-user";
}
In my view (HTML and ThymeLeaf), I am attempting to display this information. When a user has no skills, I can successfully display the user in my view. But when the user has skills, and I attempt to retrieve the skills, like this...
<tr th:each="userskill : ${userskills}">
<td th:text="${userskill.skill.name}"></td>
...I get the following error:
org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'skill' cannot be found on object of type 'uk.gov.hmrc.skills.model.UserSkill' - maybe not public or not valid?
I assumed that this was because I did not have getters and setters on my UserSkill Entity class, so I added these. But this has taken me a step back. Now, when I attempt to add a skill to a user like this...
userSkillRepository.save(userSkill);
...I enter an infinite loop.
In the raw oputput, I see this repeating seemingly infinitely:
{"id":1,"name":"Dave","email":"[email protected]","skills":[{"user":
{"id":1,"name":"Dave","email":"[email protected]","skills":[{"user":
{"id":1,"name":"Dave","email":"[email protected]","skills":[{"user":
...
This does not happen when the getters and setters are not in my UserSkill Entity class.
As a relative newcomer to JPA, I am confused and lost at this point and would hugely appreciate some help and direction!
Solution
The problem is knowed as Circular References when using spring json rest.
To fix it, add @JsonIgnoreProperties("skills") annotation to user Field on UserSkill class ( or @JsonIgnore to ignore the field when return json result).
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@Entity
@Table(name = "users_skills")
public class UserSkill {
@EmbeddedId
private UserSkillId id;
@ManyToOne
@JoinColumn(name = "fk_user", insertable = false, updatable = false)
@JsonIgnoreProperties("skills")
private User user;
@ManyToOne
@JoinColumn(name = "fk_skill", insertable = false, updatable = false)
@JsonIgnoreProperties("users")
private Skill skill;
@Column
@Enumerated(EnumType.STRING)
private Level level;
public UserSkill() {
}
...
}
2nd solution Using Jackons JSON Views
https://www.baeldung.com/jackson-json-view-annotation
3rd solution Using DTO pattern
https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application
Answered By - Mounir Messaoudi