-1

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":"dave@test.com","skills":[{"user":
{"id":1,"name":"Dave","email":"dave@test.com","skills":[{"user":
{"id":1,"name":"Dave","email":"dave@test.com","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!

Jon H
  • 394
  • 3
  • 17
  • I am not sure how JSON is coming into the picture, can you elaborate the json bit? You UI is thymeleaf, so no JSON is expected in that flow. – Himanshu Bhardwaj Jun 10 '19 at 12:13
  • Thank you for the comment Himanshu - Mounir has provided a solution below. Json is used as the content for request and response bodies of my Controller methods. – Jon H Jun 10 '19 at 12:30
  • Possible duplicate of [JSON.stringify, avoid TypeError: Converting circular structure to JSON](https://stackoverflow.com/questions/11616630/json-stringify-avoid-typeerror-converting-circular-structure-to-json) – Xavier Bouclet Jun 10 '19 at 13:19

2 Answers2

2

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() {
        }

        ...
}

https://hellokoding.com/handling-circular-reference-of-jpa-hibernate-bidirectional-entity-relationships-with-jackson-jsonignoreproperties/

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

Mounir Messaoudi
  • 343
  • 1
  • 10
  • Error mentioned by OP is `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?` – Himanshu Bhardwaj Jun 10 '19 at 12:14
  • Outstanding reply Mounir - this fixed it for me! I am so grateful because I was completely lost! :) I will mark your answer as the accepted answer but could I ask you to make a small edit first? We need to dd the @JsonIgnoreProperties annotation of the skill property too - could you just edit your code snippet to reflect this? I will wait for you to do it before accepting, because I doubt it will be editable afterwards. Thanks again. – Jon H Jun 10 '19 at 12:28
  • You can remove users field from Skill class because he has any effect. i added @JsonIgnoreProperties("users") to skills field in UserSkill if you want keep users field. – Mounir Messaoudi Jun 10 '19 at 13:44
0

@JsonBackReference annotations is solving this infinite json loop problem