【Spring Boot】About Using @ManyToMany Annotation
1. Preface
1.1 Requirements & Background
The coursework is to develop a Digital Library, which can be considered as a Paper Review Management System.
A paper may have several keywords. While searching, a keyword will relate to many papers.
Therefore, the initial idea is to design two tables in the database, one for papers, one for keywords. The relationship between these two tables is **many-to-many**. (In fact, another method was found later. However, such method was not been taken advantage. The development process followed the initial idea.)
Some problems occurred. Consequently, this article is written to summarize the process of solving the problems. The code shown in this article reflects this process, and has been published on GitHub. You can clone it, and then comment or uncomment different parts to see how it works.
1.2 Supplementary Explanation
This is the first time for me to use Spring Boot
framework in order to implement the coursework. Previously, I used to work with Django
and Flask
, which are written in python
. Thus, the project shown in this article keeps the previous habits. However, the project submitted for the coursework certainly takes the habits of Spring Boot
.
There is also a Chinese version of this article.
2. The Code
2.1 Model Layer
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) // Give the currently set entity the authority to operate another entity.
private Set<Keyword> keywords;
}
@ManyToMany
annotation is used to specify the many-to-many relationship between two tables. The cascade
attribute is added in the @ManyToMany
annotation in Paper.java
, whose value is CascadeType.PERSIST
, which means
Give the currently set entity the authority to operate another entity.^[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;
}
The mappedBy
attribute is added in the @ManyToMany
annotation in Keyword.java
, which means
the side where it is located is the owned side, and the side it points to is the owner.^[3][4]^
Once runs, the program will automatically create a paper
table, a keyword
table, and a paper_keywords
middle table to describe the relationship.
If the mappedBy
attribute is not used, two middle tables will be created, which is not necessary and not concise.
It can be told from the above code that, in aiming of simplify the code, @Data
annotation is used, which led to a hidden trouble for the subsequent development.
2.2 Controller Layer
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();
}
}
Two simple APIs are implemented to response to GET requests and POST requests respectively.
According to the official document^[5]^, PaperRepository
extends CrudRepository
interface, which will not discuss in detail here.
3. Data Format of POST Requests
3.1 JSON
In the Controller, the method where POST requests mapped to takes a Paper object as parameter. As a consequence, the input JSON data can be deserialized to a Paper object. However, the JSON data can only be deserialized if it is in the right format.
An example of the right format:
{
"title": "pure json",
"keywords": [
{"keyword": "CS"},
{"keyword": "DS"},
{"keyword": "MIS"}
]
}
3.2 cURL
While using cURL under Windows cmd to send JSON data, the data itself should be marked by double quotation marks, and the double quotation marks in the JSON data should be labeled with backslash escapes. The following is an example (the port number of this project is set to be 8081, the same below):
curl -X POST -H "content-type: application/json" -d "{\"title\": \"from_curl\", \"keywords\": [{\"keyword\": \"BS\"}]}" http://localhost:8081/paper/add
In the example,
-X
parameter is used to specify the method of HTTP request,-H
parameter is used to add HTTP header, which is not sensitive to case and space, and specifies the content type to be sent,-d
parameter is used to add the data body of a POST request, which will automatically send a POST request if used, and thus-X POST
can be omitted,- at last is the url where the request to be sent to.
For more information on the usage of cURL, please refer to this article (in Chinese)^[6]^.
3.3 Python
The requests
package of Python can be taken advantage of to send HTTP requests. The code that I use to test this project is shown as follows:
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 Supplementary
Personally speaking, the format of data given in 3.1 is not ideal. A simpler format that is also easier for frontend to operate could be like:
{
"title": "pure json",
"keywords": ["CS", "DS", "MIS"]
}
However, it is not realized. I sincerely appreciate it if someone could tell a way to achieve it!
4. Error Occurs When Sending GET Requests
4.1 Error Information
Build and run the previously given program, data can be successfully received by the backend. But while trying to get data, the following error messages will be shown:
*** 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>
The contents of the beginning three lines may appear more than three times. The contents after the fifth line will also show up for many times. The repeated contents are omitted.
4.2 Reason of Error
Such situation is due to the StackOverflowError
caused by infinite recursion
. The reason why infinite recursion occurs is stated below.
Primarily, in the model layer, the keywords
field in Paper
is related to Keyword
, and the papers
field in Keyword
is related to Paper
. Assume that the data of all Paper
is requested. In this case, when serializing, the data of a paper
object will be loaded. Then, all keyword
objects relate to that paper
object will consequently be loaded. After that, all paper
objects relate to those keyword
objects will also be loaded. However, these paper
objects include the paper
object loaded at the beginning. As a result, The above process will continue to loop until the stack space is insufficient.
4.3 Solution
The key of solving this issue is to stop the infinite recursion. Additionally, enough data should have been loaded.
The code in the controller layer does not have to be changed.
The @Data
annotation must be deleted in the model layer. Meanwhile, in order to keep the code concise, @Setter
and @Getter
annotations can be used. Then, the annotation @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
should be added before the declaration of class. Consequently, a serialized object will not be serialized again. Because of this, the infinite recursion is interrupted and enough data is loaded.
The final code is shown below:
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) // Give the currently set entity the authority to operate another entity.
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 Supplementary
This session may look like a free pen.
It can be told from the project of this article^[1]^ that, considerable number of methods on the Internet have been tried to use. However, none of them got the effect shown in the corresponding articles, which really confused me. I have tried to use annotations such as @JsonIgnore
, @JsonIgnoreProperties
, @JsonManagedReference
, @JsonBackReference
, etc. Additionally, I have tried to manually delete the part that causes infinite recursion. Moreover, I have also tried to write a serializer on my own. However, none of these methods works. The error information may change a little bit, but it always exists.
Finally, I accidentally found that, using methods on the Internet after deleting @Data
annotation would work. And then I realized that it is because of @Data
annotation.
Understood the cause, I adopted the method described in 4.3 after comparing.
I have also considered to figure out the reason why @Data
annotation would lead to infinite recursion. But at present, I only know that @Data
can be used as a simplified equivalent substitute for some other annotations. Limited by the deadline of the coursework submission, I have to focus on more necessary parts. And there may be no time for me to study this part in a period of time. I also sincerely appreciate it if someone knows it and is willing to tell something about it!
5. Reference
Most of the references in this article have been marked in the corresponding position in the form of links. The following is a complete list of references:
[1] Night-Voyager. (2021, Apr. 20). AManyToManyTestDemo. Retrieved Apr. 26, 2021, from https://github.com/Night-Voyager/AManyToManyTestDemo.
[2] Osheep. (2017, Aug. 25). 【简单易懂】JPA概念解析:CascadeType(各种级联操作)详解。. Retrieved Apr. 26, 2021, from 架构修炼: https://www.osheep.cn/3680.html. (in Chinese)
[3] 笙歌会停. (2019, Oct. 26). @ManyToMany中的mappedy. Retrieved Apr. 26, 2021, from SegmentFault 思否: https://segmentfault.com/a/1190000020806546. (in Chinese, contains typos)
[4] NimChimpsky, & JB Nizet. (2013, Jan. 1). @ManyToMany(mappedBy = “foo”). Retrieved Apr. 26, 2021, from Stack Overflow: https://stackoverflow.com/questions/14111607/manytomanymappedby-foo.
[5] VMware, Inc. or its affiliates. (2021, Mar. 11). Getting Started | Accessing data with MySQL. Retrieved Mar. 19, 2021, from Spring: https://spring.io/guides/gs/accessing-data-mysql/.
[6] 阮一峰. (2019, Sept. 5). curl 的用法指南. Retrieved Apr. 6, 2021, from 阮一峰的网络日志: http://www.ruanyifeng.com/blog/2019/09/curl-reference.html. (in Chinese)