JPA 一对多双向堆栈溢出问题
JPA One To Many Bi-Directional Stack Overflow Issue
我正在使用 Java 1.8、Spring Boot、REST、JPA 创建一个 Spring Boot REST 微服务 API,它的实体关系具有以下基数:
Owner can have many Cars.
Cars only have one Owner.
我能够通过我的 REST Web 服务创建和查看所有者。
每次我尝试创建具有关联所有者的汽车时,它都会正确填充数据库的行,但似乎它会无限递归导致堆栈溢出错误(见下文)。
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.myapi</groupId>
<artifactId>car-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>car-api</name>
<description>Car REST API</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
src/main/resources/applications.属性:
server.servlet.context-path=/car-api
server.port=8080
server.error.whitelabel.enabled=false
# Database specific
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mysql://localhost:3306/car_db?useSSL=false
spring.datasource.ownername=root
spring.datasource.password=
所有者实体:
@Entity
@Table(name = "owner")
public class Owner {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String name;
@OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "owner")
private List<Car> cars = new ArrayList<>();
public Owner() {
}
// Getter & Setters omitted for brevity.
}
汽车实体:
@Entity
@Table(name="car")
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
String make;
String model;
String year;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner_id", nullable = false)
private Owner owner;
// Getter & Setters omitted for brevity.
}
所有者存储库:
@Repository
public interface OwnerRepository extends JpaRepository<Owner, Long> {
}
汽车资料库:
@Repository
public interface CarRepository extends JpaRepository<Car, Long> {
}
所有者服务:
public interface OwnerService {
boolean createOwner(Owner owner);
Owner getOwnerByOwnerId(Long ownerId);
List<Owner> getAllOwners();
}
OwnerServiceImpl:
@Service
public class OwnerServiceImpl implements OwnerService {
@Autowired
OwnerRepository ownerRepository;
@Autowired
CarRepository carRepository;
@Override
public List<Owner> getAllOwners() {
return ownerRepository.findAll();
}
@Override
public boolean createOwner(Owner owner) {
boolean created = false;
if (owner != null) {
ownerRepository.save(owner);
created = true;
}
return created;
}
@Override
public Owner getOwnerByOwnerId(Long ownerId) {
Optional<Owner> owner = null;
if (ownerRepository.existsById(ownerId)) {
owner = ownerRepository.findById(ownerId);
}
return owner.get();
}
}
汽车服务:
public interface CarService {
boolean createCar(Long ownerId, Car car);
}
CarServiceImpl:
@Service
public class CarServiceImpl implements CarService {
@Autowired
OwnerRepository ownerRepository;
@Autowired
CarRepository carRepository;
@Override
public boolean createCar(Long ownerId, Car car) {
boolean created = false;
if (ownerRepository.existsById(ownerId)) {
Optional<Owner> owner = ownerRepository.findById(ownerId);
if (owner != null) {
List<Car> cars = owner.get().getCars();
cars.add(car);
owner.get().setCars(cars);
car.setOwner(owner.get());
carRepository.save(car);
created = true;
}
}
return created;
}
}
所有者控制器:
@RestController
public class OwnerController {
private HttpHeaders headers = null;
@Autowired
OwnerService ownerService;
public OwnerController() {
headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
}
@RequestMapping(value = { "/owners" }, method = RequestMethod.POST, produces = "APPLICATION/JSON")
public ResponseEntity<Object> createOwner(@Valid @RequestBody Owner owner) {
boolean isCreated = ownerService.createOwner(owner);
if (isCreated) {
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
else {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
}
@RequestMapping(value = { "/owners" }, method = RequestMethod.GET, produces = "APPLICATION/JSON")
public ResponseEntity<Object> getAllOwners() {
List<Owner> owners = ownerService.getAllOwners();
if (owners.isEmpty()) {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Object>(owners, headers, HttpStatus.OK);
}
@RequestMapping(value = { "/owners/{ownerId}" }, method = RequestMethod.GET, produces = "APPLICATION/JSON")
public ResponseEntity<Object> getOwnerByOwnerId(@PathVariable Long ownerId) {
if (null == ownerId || "".equals(ownerId)) {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
Owner owner = ownerService.getOwnerByOwnerId(ownerId);
return new ResponseEntity<Object>(owner, headers, HttpStatus.OK);
}
}
汽车控制器:
@RestController
public class CarController {
private HttpHeaders headers = null;
@Autowired
CarService carService;
public VehicleController() {
headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
}
@RequestMapping(value = { "/cars/{ownerId}" }, method = RequestMethod.POST, produces = "APPLICATION/JSON")
public ResponseEntity<Object> createVehicleBasedOnOwnerId(@Valid @RequestBody Car car, Long ownerId) {
boolean isCreated = carService.createCar(ownerId, vehicle);
if (isCreated) {
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
else {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
}
然而,我可以创建新所有者(并在数据库中查看它们,还可以通过 curl / Postman 调用 getAllOwners 来查看它们),方法是将其作为请求正文传递:
{
"owner": "John Doe"
}
数据库里面car_db.owner
:
-------------------------------------
|id | name |
-------------------------------------
|1 | John Doe |
-------------------------------------
当我尝试使用此 REST 调用为车主创建全新汽车时出现问题 /cars/{ownerId}
:
POST http://localhost:8080/car-api/cars/1
请求正文如下:
{
"make": "Honda",
"model": "Accord"
"year": 2020
}
它将它正确地插入到 MySQL 数据库的 car_db.car
table 中,如下所示:
---------------------------------------
|id | make | model | year | owner_id|
---------------------------------------
|1 | Honda | Accord | 2020 | 1 |
---------------------------------------
我在里面做了什么 CarServiceImpl.createCar() method
它导致双向关系破裂?
创建 Stack Over Flow:空执行:
-03-08 01:43:20,106 ERROR org.apache.juli.logging.DirectJDKLog [http-nio-8080-exec-1] Servlet.service() for servlet [dispatcherServlet] in context with path [/car-api] threw exception [Handler dispatch failed; nested exception is java.lang.WhosebugError] with root cause
java.lang.WhosebugError: null
at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:512)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:141)
at com.myapi.model.Car.toString(Car.java:87)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.myapi.model.Owner.toString(Owner.java:105)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.myapi.model.Car.toString(Car.java:87)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.myapi.model.Owner.toString(Owner.java:105)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.myapi.model.Car.toString(Car.java:87)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
奇怪的是,尽管这是我每次创建新车时都会出现的堆栈跟踪,但数据库中的一切都很好(插入在车内 table 并具有正确的 ownerId在行内),当我执行以下任一 GET 请求时,我能够查看 JSON 响应负载:
GET http://localhost:8080/owners/1
产量:
{
"name": "John Doe",
"cars": [
{
"make": "Honda",
"model": "Accord",
"year": 2020
}
]
}
GET http://localhost:8080/owners
产量:
[
{
"name": "John Doe",
"cars": [
{
"make": "Honda",
"model": "Accord",
"year": 2020
}
]
}
]
尽管所有 GET 和数据库插入都在工作,但为什么我会收到此堆栈溢出错误?
JPA 与此错误无关。查看堆栈跟踪 - 您的 Car#toString()
打印其 Owner
。 Owner#toString()
打印其 Cars 集合。
因此,当您的代码中的某些内容对这些对象之一调用 toString()
时 - 它会导致无限的调用链,只有在达到线程堆栈的最大深度时才会结束,从而导致 WhosebugError。
通常在 toString()
中我们只想从当前 class 打印 primitives/ValueObjects。如果我们也开始打印关联的实体 - 这将导致惰性字段被初始化。
在我的所有者 class 中,这是 IntelliJ IDEA 为我的 toString()
方法生成的内容:
public String toString() {
return "Owner{" +
"id=" + id +
"name=" + name +
", cars=" + cars +
'}';
}
当我把包含", cars=" + cars +
的那一行去掉后,就变成了:
public String toString() {
return "Owner{" +
"id=" + id +
"name=" + name +
'}';
}
成功了!
我正在使用 Java 1.8、Spring Boot、REST、JPA 创建一个 Spring Boot REST 微服务 API,它的实体关系具有以下基数:
Owner can have many Cars.
Cars only have one Owner.
我能够通过我的 REST Web 服务创建和查看所有者。
每次我尝试创建具有关联所有者的汽车时,它都会正确填充数据库的行,但似乎它会无限递归导致堆栈溢出错误(见下文)。
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.myapi</groupId>
<artifactId>car-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>car-api</name>
<description>Car REST API</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
src/main/resources/applications.属性:
server.servlet.context-path=/car-api
server.port=8080
server.error.whitelabel.enabled=false
# Database specific
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:mysql://localhost:3306/car_db?useSSL=false
spring.datasource.ownername=root
spring.datasource.password=
所有者实体:
@Entity
@Table(name = "owner")
public class Owner {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String name;
@OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "owner")
private List<Car> cars = new ArrayList<>();
public Owner() {
}
// Getter & Setters omitted for brevity.
}
汽车实体:
@Entity
@Table(name="car")
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
String make;
String model;
String year;
@JsonIgnore
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "owner_id", nullable = false)
private Owner owner;
// Getter & Setters omitted for brevity.
}
所有者存储库:
@Repository
public interface OwnerRepository extends JpaRepository<Owner, Long> {
}
汽车资料库:
@Repository
public interface CarRepository extends JpaRepository<Car, Long> {
}
所有者服务:
public interface OwnerService {
boolean createOwner(Owner owner);
Owner getOwnerByOwnerId(Long ownerId);
List<Owner> getAllOwners();
}
OwnerServiceImpl:
@Service
public class OwnerServiceImpl implements OwnerService {
@Autowired
OwnerRepository ownerRepository;
@Autowired
CarRepository carRepository;
@Override
public List<Owner> getAllOwners() {
return ownerRepository.findAll();
}
@Override
public boolean createOwner(Owner owner) {
boolean created = false;
if (owner != null) {
ownerRepository.save(owner);
created = true;
}
return created;
}
@Override
public Owner getOwnerByOwnerId(Long ownerId) {
Optional<Owner> owner = null;
if (ownerRepository.existsById(ownerId)) {
owner = ownerRepository.findById(ownerId);
}
return owner.get();
}
}
汽车服务:
public interface CarService {
boolean createCar(Long ownerId, Car car);
}
CarServiceImpl:
@Service
public class CarServiceImpl implements CarService {
@Autowired
OwnerRepository ownerRepository;
@Autowired
CarRepository carRepository;
@Override
public boolean createCar(Long ownerId, Car car) {
boolean created = false;
if (ownerRepository.existsById(ownerId)) {
Optional<Owner> owner = ownerRepository.findById(ownerId);
if (owner != null) {
List<Car> cars = owner.get().getCars();
cars.add(car);
owner.get().setCars(cars);
car.setOwner(owner.get());
carRepository.save(car);
created = true;
}
}
return created;
}
}
所有者控制器:
@RestController
public class OwnerController {
private HttpHeaders headers = null;
@Autowired
OwnerService ownerService;
public OwnerController() {
headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
}
@RequestMapping(value = { "/owners" }, method = RequestMethod.POST, produces = "APPLICATION/JSON")
public ResponseEntity<Object> createOwner(@Valid @RequestBody Owner owner) {
boolean isCreated = ownerService.createOwner(owner);
if (isCreated) {
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
else {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
}
@RequestMapping(value = { "/owners" }, method = RequestMethod.GET, produces = "APPLICATION/JSON")
public ResponseEntity<Object> getAllOwners() {
List<Owner> owners = ownerService.getAllOwners();
if (owners.isEmpty()) {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Object>(owners, headers, HttpStatus.OK);
}
@RequestMapping(value = { "/owners/{ownerId}" }, method = RequestMethod.GET, produces = "APPLICATION/JSON")
public ResponseEntity<Object> getOwnerByOwnerId(@PathVariable Long ownerId) {
if (null == ownerId || "".equals(ownerId)) {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
Owner owner = ownerService.getOwnerByOwnerId(ownerId);
return new ResponseEntity<Object>(owner, headers, HttpStatus.OK);
}
}
汽车控制器:
@RestController
public class CarController {
private HttpHeaders headers = null;
@Autowired
CarService carService;
public VehicleController() {
headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
}
@RequestMapping(value = { "/cars/{ownerId}" }, method = RequestMethod.POST, produces = "APPLICATION/JSON")
public ResponseEntity<Object> createVehicleBasedOnOwnerId(@Valid @RequestBody Car car, Long ownerId) {
boolean isCreated = carService.createCar(ownerId, vehicle);
if (isCreated) {
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
else {
return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
}
}
然而,我可以创建新所有者(并在数据库中查看它们,还可以通过 curl / Postman 调用 getAllOwners 来查看它们),方法是将其作为请求正文传递:
{
"owner": "John Doe"
}
数据库里面car_db.owner
:
-------------------------------------
|id | name |
-------------------------------------
|1 | John Doe |
-------------------------------------
当我尝试使用此 REST 调用为车主创建全新汽车时出现问题 /cars/{ownerId}
:
POST http://localhost:8080/car-api/cars/1
请求正文如下:
{
"make": "Honda",
"model": "Accord"
"year": 2020
}
它将它正确地插入到 MySQL 数据库的 car_db.car
table 中,如下所示:
---------------------------------------
|id | make | model | year | owner_id|
---------------------------------------
|1 | Honda | Accord | 2020 | 1 |
---------------------------------------
我在里面做了什么 CarServiceImpl.createCar() method
它导致双向关系破裂?
创建 Stack Over Flow:空执行:
-03-08 01:43:20,106 ERROR org.apache.juli.logging.DirectJDKLog [http-nio-8080-exec-1] Servlet.service() for servlet [dispatcherServlet] in context with path [/car-api] threw exception [Handler dispatch failed; nested exception is java.lang.WhosebugError] with root cause
java.lang.WhosebugError: null
at java.base/java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:512)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:141)
at com.myapi.model.Car.toString(Car.java:87)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.myapi.model.Owner.toString(Owner.java:105)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.myapi.model.Car.toString(Car.java:87)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.myapi.model.Owner.toString(Owner.java:105)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at com.myapi.model.Car.toString(Car.java:87)
at java.base/java.lang.String.valueOf(String.java:2788)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:473)
at org.hibernate.collection.internal.PersistentBag.toString(PersistentBag.java:622)
奇怪的是,尽管这是我每次创建新车时都会出现的堆栈跟踪,但数据库中的一切都很好(插入在车内 table 并具有正确的 ownerId在行内),当我执行以下任一 GET 请求时,我能够查看 JSON 响应负载:
GET http://localhost:8080/owners/1
产量:
{
"name": "John Doe",
"cars": [
{
"make": "Honda",
"model": "Accord",
"year": 2020
}
]
}
GET http://localhost:8080/owners
产量:
[
{
"name": "John Doe",
"cars": [
{
"make": "Honda",
"model": "Accord",
"year": 2020
}
]
}
]
尽管所有 GET 和数据库插入都在工作,但为什么我会收到此堆栈溢出错误?
JPA 与此错误无关。查看堆栈跟踪 - 您的 Car#toString()
打印其 Owner
。 Owner#toString()
打印其 Cars 集合。
因此,当您的代码中的某些内容对这些对象之一调用 toString()
时 - 它会导致无限的调用链,只有在达到线程堆栈的最大深度时才会结束,从而导致 WhosebugError。
通常在 toString()
中我们只想从当前 class 打印 primitives/ValueObjects。如果我们也开始打印关联的实体 - 这将导致惰性字段被初始化。
在我的所有者 class 中,这是 IntelliJ IDEA 为我的 toString()
方法生成的内容:
public String toString() {
return "Owner{" +
"id=" + id +
"name=" + name +
", cars=" + cars +
'}';
}
当我把包含", cars=" + cars +
的那一行去掉后,就变成了:
public String toString() {
return "Owner{" +
"id=" + id +
"name=" + name +
'}';
}
成功了!