美文网首页
【Spring Boot】关于@ManyToMany的使用

【Spring Boot】关于@ManyToMany的使用

作者: 焜_8899 | 来源:发表于2021-05-05 22:00 被阅读0次

    1. 写在前面

    1.1 需求背景

    学校作业让开发一个“数字图书馆”,具体要求可以理解为一个论文评审管理系统。

    一篇论文会有多个关键词。而搜索的时候,一个关键词会对应多个论文。

    所以,最初设计数据库时的想法是,论文一个表,关键字一个表,<u>两个表之间是多对多的关系</u>。(其实后来发现可能有别的方案,但没有尝试,就按照当时的思路做下去了。)

    这之后遇到了一些问题,于是在这篇文章整理下过程与解决方法。体现这一过程的代码已经发布到了GitHub上[1],亦是本文中的内容。有需要的可以下载,也可以尝试注释掉或取消注释不同的部分,看看效果。( ̄︶ ̄

    1.2 补充说明

    这个作业是本人第一次接触Spring Boot。在这之前,本人学习的是用python语言开发的DjangoFlask框架。因此,作为本文示例的项目中的目录结构保留了之前的习惯。当然,用于交作业的项目的目录结构采取的是Spring Boot的习惯。( ̄▽ ̄

    因为本课程教师是外教,所以本文提供了英文版本

    2. 代码部分

    2.1 模型层(Model)

    Paper.java
    package com.example.demo.paper;
    
    import com.example.demo.keyword.Keyword;
    import lombok.Data;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import javax.persistence.*;
    import java.util.Set;
    
    @Data
    @Entity
    @EntityListeners(AuditingEntityListener.class)
    public class Paper {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        private String title;
    
        @ManyToMany(cascade = CascadeType.PERSIST) // 给当前设置的实体操作另一个实体的权限。
        private Set<Keyword> keywords;
    }
    
    

    其中使用了@ManyToMany注解,表示两表之间是多对多关系。Paper.java@ManyToMany注解中加入了cascade属性,其值为CascadeType.PERSIST,表示

    给当前设置的实体操作另一个实体的权限。[2]

    Keyword.java
    package com.example.demo.keyword;
    
    import com.example.demo.paper.Paper;
    import lombok.Data;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import javax.persistence.*;
    import java.util.Set;
    
    @Data
    @Entity
    @EntityListeners(AuditingEntityListener.class)
    public class Keyword {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        private String keyword;
    
        @ManyToMany(mappedBy = "keywords")
        private Set<Paper> papers;
    }
    
    

    Keyword.java@ManyToMany注解中加入了mappedBy属性,表示

    其所在的一方是被拥有方,而其指向的是拥有方。[3][4]

    运行程序后,会自动生成paper表和keyword表,以及一个paper_keywords中间表。

    假如没有在@ManyToMany注解中加入mappedBy属性的话,会生成两张中间表,没有必要也不够简洁。

    在以上代码中可以看到,为了简化代码,使用了@Data注解。这为后续的开发埋下了隐患。

    2.2 控制层(Controller)

    PaperController.java
    package com.example.demo.paper;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.*;
    
    @Controller
    @RequestMapping(path = "/paper")
    public class PaperController {
        @Autowired
        private PaperRepository paperRepository;
    
        @GetMapping(path = "/all")
        public @ResponseBody Iterable<Paper> getAllPapers() {
            return paperRepository.findAll();
        }
    
        @PostMapping(path = "/add")
        @ResponseBody
        public ResponseEntity<?> addNewPaper(@RequestBody Paper paper) {
            paperRepository.save(paper);
            return ResponseEntity.status(HttpStatus.CREATED).build();
        }
    }
    
    

    这样就实现了两个简单的接口,分别响应GET请求和POST请求。

    其中,PaperRepository根据官方文档[5]拓展了CrudRepository接口,在此不做详细展开。

    3. POST请求数据格式

    3.1 JSON

    在控制层中,POST请求被映射到的函数接收的参数是个Paper对象。这样,传入接口的JSON数据可以被反序列化为Paper对象。然而,只有当传入的JSON格式正确时,才能进行反序列化。

    In the Controller, the method that POST requests are mapped to takes a Paper object as parameter.

    一个格式正确的例子:

    {
        "title": "pure json",
        "keywords": [
            {"keyword": "CS"},
            {"keyword": "DS"},
            {"keyword": "MIS"}
        ]
    }
    

    3.2 cURL

    假如在Windows的命令行下使用cURL发送JSON格式的数据,那么所传的JSON数据本身需要用双引号引起来,而JSON数据里面的双引号需要加反斜杠进行转义。下面是一个例子(本文项目端口号被设为8081,下同):

    curl -X POST -H "content-type: application/json" -d "{\"title\": \"from_curl\", \"keywords\": [{\"keyword\": \"BS\"}]}" http://localhost:8081/paper/add
    

    其中:

    • -X参数用于指定HTTP请求的方法;
    • -H参数用于添加HTTP标头,对大小写和空格不敏感,这里指定了发送的内容类型;
    • -d参数用于添加发送POST请求的数据体,使用后将自动使用POST方法发送请求,因此可省略-X POST
    • 最后是请求的地址。

    更多关于cURL的用法可以参考这篇文章[6]

    3.3 Python

    Python中的requests包可以用来发送HTTP请求。本人用于测试本项目的代码如下:

    import requests
    
    data = {
        "title": "from_python",
        "keywords": [
            {"keyword": "CS"},
            {"keyword": "DS"},
            {"keyword": "MIS"}
        ]
    }
    
    print(
        requests.post(
            url='http://127.0.0.1:8081/paper/add',
            json=data
        ).text
    )
    
    

    3.4 补充

    个人认为,3.1中所给的数据格式其实不是很理想。更加简单且便于前端操作的格式应当是:

    {
        "title": "pure json",
        "keywords": ["CS", "DS", "MIS"]
    }
    

    然而并没有实现。若是有人能告知解决方法,感激不尽!

    4. GET请求无限递归报错

    4.1 报错信息

    将以上程序编译运行,可以正常插入数据。但当读取数据时,出现类似以下报错:

    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    *** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message transform method call failed at JPLISAgent.c line: 844
    2021-04-28 17:38:01.558  WARN 9476 --- [nio-8081-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: java.util.ArrayList[0]->com.example.demo.paper.Paper["keywords"])]
    2021-04-28 17:38:01.560  WARN 9476 --- [nio-8081-exec-1] o.h.e.loading.internal.LoadContexts      : HHH000100: Fail-safe cleanup (collections) : org.hibernate.engine.loading.internal.CollectionLoadContext@2c52ae69<rs=HikariProxyResultSet@1263489024 wrapping Result set representing update count of -1>
    2021-04-28 17:38:01.560  WARN 9476 --- [nio-8081-exec-1] o.h.e.loading.internal.LoadContexts      : HHH000100: Fail-safe cleanup (collections) : org.hibernate.engine.loading.internal.CollectionLoadContext@7af84376<rs=HikariProxyResultSet@1287874177 wrapping Result set representing update count of -1>
    2021-04-28 17:38:01.561  WARN 9476 --- [nio-8081-exec-1] o.h.e.loading.internal.LoadContexts      : HHH000100: Fail-safe cleanup (collections) : org.hibernate.engine.loading.internal.CollectionLoadContext@59b5f55c<rs=HikariProxyResultSet@2018225027 wrapping Result set representing update count of -1>
    

    其中前三行的内容可能会出现不止三次,第5行以后的内容会重复多次,重复的内容省略。

    4.2 报错原因

    出现这种情况,原因是存在无限递归(Infinite recursion)导致的栈溢出错误(StackOverflowError)。

    而之所以会出现无限递归,原因如下:

    首先,在模型层,Paper中的keywords字段关联向了KeywordKeyword中的papers字段又关联回了Paper。假设现在要读取所有Paper的数据。这种情况下,在进行序列化的时候,一个paper对象的数据会被加载。然后,其所关联的所有keyword对象的数据也会被加载。这时,与这些keyword相关联的paper对象的数据也都会被加载。而这些paper中就包含刚才已经被加载过的那一个paper。于是,上述过程会一直持续循环,直到栈空间不足。

    4.3 处理方法

    要解决这一问题,关键点在于终止无限递归。当然,此时需要加载完足够的数据。

    上述控制层的代码不需要更改。

    模型层的代码,须将@Data注解删除。同时,为了保持对代码的简化,可使用@Setter@Getter注解。然后,在类的声明之前加上@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")。这样,一个被序列化过的对象将不会再次被序列化,从而既终止了无限递归又保证加载了足够的数据。

    最终代码如下:

    Paper.java
    package com.example.demo.paper;
    
    import com.example.demo.keyword.Keyword;
    import com.fasterxml.jackson.annotation.*;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import javax.persistence.*;
    import java.util.Set;
    
    @Entity
    @EntityListeners(AuditingEntityListener.class)
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
    @Setter
    @Getter
    public class Paper {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        private String title;
    
        @ManyToMany(cascade = CascadeType.PERSIST) // 给当前设置的实体操作另一个实体的权限。
        private Set<Keyword> keywords;
    }
    
    
    Keyword.java
    package com.example.demo.keyword;
    
    import com.example.demo.paper.Paper;
    import com.fasterxml.jackson.annotation.*;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import javax.persistence.*;
    import java.util.Set;
    
    @Entity
    @EntityListeners(AuditingEntityListener.class)
    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
    @Setter
    @Getter
    public class Keyword {
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Integer id;
    
        private String keyword;
    
        @ManyToMany(mappedBy = "keywords")
        private Set<Paper> papers;
    }
    
    

    4.4 补充

    这一段可能会看着像个闲笔。

    本文的项目[1]可看出,我已经尝试过很多网上建议的方法,但当时全部都没有得到相应文章所说的效果。我一度感到疑惑。我试过使用@JsonIgnore@JsonIgnoreProperties@JsonManagedReference@JsonBackReference等注解,也试过在controller中手动删除会导致无限递归的部分,更试过自己写一个序列化器。然而,结果都只是报错的信息略有变化,但报错的情况始终存在。

    最终,我偶然试出,在删除@Data注解后,再使用网上的方法,可以实现相应的效果,才意识到可能是@Data注解的问题。

    发现这个问题后,再经过对比,我才最终采用本文4.3小节中说明的方法。

    我也想过试着找一下@Data注解会导致无限递归的原因。但目前只了解到@Data可以作为另外几个注解的简化的等效替代。受限于作业提交的时间,我只得把注意力放到相对更要紧的地方,短时间内应该没有时间专研这一块的内容。若是有人有所了解,愿意交流,亦感激不尽!

    5. 参考文献

    本文大多参考文献已以链接的形式标注在了相应位置。以下是完整参考文献列表:

    [1] Night-Voyager. AManyToManyTestDemo[EB/OL]. (2021-04-20) [2021-04-26]. https://github.com/Night-Voyager/AManyToManyTestDemo.

    [2] Osheep. 【简单易懂】JPA概念解析:CascadeType(各种级联操作)详解。 - 架构修炼[EB/OL]. (2017-08-25) [2021-04-26]. https://www.osheep.cn/3680.html.

    [3] 笙歌会停. @ManyToMany中的mappedy - SegmentFault 思否[EB/OL]. (2019-10-26) [2021-04-26]. https://segmentfault.com/a/1190000020806546. (这篇文章中有拼写错误)

    [4] NimChimpsky 和 JB Nizet. java - @ManyToMany(mappedBy = "foo") - Stack Overflow[EB/OL]. (2013-01-01) [2021-04-26]. https://stackoverflow.com/questions/14111607/manytomanymappedby-foo.

    [5] VMware, Inc. or its affiliates. Getting Started | Accessing data with MySQL[EB/OL]. (2021-03-11) [2021-03-19]. https://spring.io/guides/gs/accessing-data-mysql/.

    [6] 阮一峰. curl 的用法指南 - 阮一峰的网络日志[EB/OL]. (2019-09-05) [2021-04-06]. http://www.ruanyifeng.com/blog/2019/09/curl-reference.html.

    相关文章

      网友评论

          本文标题:【Spring Boot】关于@ManyToMany的使用

          本文链接:https://www.haomeiwen.com/subject/mcildltx.html