JPA Spring Boot 微服务 - 使用两个多对一映射持久化实体时的无限循环

Posted

技术标签:

【中文标题】JPA Spring Boot 微服务 - 使用两个多对一映射持久化实体时的无限循环【英文标题】:JPA Spring Boot micro service - Infinite loop when persisting an Entity with two ManyToOne mappings 【发布时间】:2019-10-24 18:07:34 【问题描述】:

遵循这篇文章中给出的优秀建议...

How to model a three-way relationship in a JPA Spring Boot micro service with a mysql back end

..我在 Spring-boot Java 应用程序中编写了三个实体类(如下所示)来表示我所在组织的技能库,其中每个用户可能有很多技能。 'Level' 枚举属性用于表示用户对每个特定技能的能力。

我添加了一些控制器映射,启动了我的应用程序并进行了一些基本的 API 测试,其中我能够成功地为用户添加技能并指定附加级别属性。

这里是实体类:

@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);
    

我正在使用代表三个实体的存储库类为用户分配技能:

        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";
    

在我看来(html 和 ThymeLeaf),我正在尝试显示此信息。 当用户没有技能时,我可以成功地将用户显示在我的视图中。但是当用户有技能时,我尝试检索技能,就像这样......

<tr th:each="userskill : $userskills">
  <td th:text="$userskill.skill.name"></td>

...我收到以下错误:

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?

我认为这是因为我的 UserSkill 实体类中没有 getter 和 setter,所以我添加了这些。但这让我后退了一步。现在,当我尝试向这样的用户添加技能时......

        userSkillRepository.save(userSkill);

...我进入了一个无限循环。

在原始输出中,我看到这似乎无限重复:

"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":
...

当 getter 和 setter 不在我的 UserSkill 实体类中时,不会发生这种情况。

作为 JPA 的新手,我在这一点上感到困惑和迷茫,非常感谢一些帮助和指导!

【问题讨论】:

我不确定 JSON 是如何出现的,你能详细说明一下 json 位吗?您的 UI 是 thymeleaf,因此该流程中不需要 JSON。 感谢您的评论 Himanshu - Mounir 在下面提供了解决方案。 Json 用作我的 Controller 方法的请求和响应主体的内容。 JSON.stringify, avoid TypeError: Converting circular structure to JSON的可能重复 【参考方案1】:

在使用 spring json rest 时,这个问题被称为循环引用。

要修复它,请在 UserSkill 类的 user 字段中添加 @JsonIgnoreProperties("skills") 注释(或 @JsonIgnore 以在返回 json 结果时忽略该字段)。

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/

使用 Jackons JSON 视图的第二个解决方案

https://www.baeldung.com/jackson-json-view-annotation

使用 DTO 模式的第三种解决方案

https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application

【讨论】:

OP提到的错误是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? 出色的回复 Mounir - 这为我解决了问题!我很感激,因为我完全迷路了! :) 我会将您的答案标记为已接受的答案,但我可以请您先做一个小编辑吗?我们还需要 dd 技能属性的 @JsonIgnoreProperties 注释 - 你可以编辑你的代码 sn-p 来反映这一点吗?我会在接受之前等你做,因为我怀疑它之后是否可以编辑。再次感谢。 您可以从 Skill 类中删除 users 字段,因为他有任何影响。如果您想保留用户字段,我将 @JsonIgnoreProperties("users") 添加到 UserSkill 的技能字段中。【参考方案2】:

@JsonBackReference annotations 正在解决这个无限的 json 循环问题

【讨论】:

以上是关于JPA Spring Boot 微服务 - 使用两个多对一映射持久化实体时的无限循环的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Spring Security 实现具有两级令牌认证的 Spring Boot 微服务?

如何为 Spring Boot JPA 时间戳指定 UTC 时区

Spring Boot - JPA 或 SQL 查询在连接表中查找最常见的项目?

Spring Boot JPA 元模型不能为空!尝试运行 JUnit / 集成测试时

如何跨单个项目的不同 Spring Boot 微服务实现 JPA 审计?

两小时上手Spring Boot (开发红包程序———通过JPA 连接Mysql ,事务等内容) 遇到的问题