JPA Один-ко-многим Двунаправленная проблема переполнения стека - PullRequest
0 голосов
/ 07 марта 2020

Я использую Java 1.8, Spring Boot, REST, JPA для создания API-интерфейса Spring Boot REST Microservice, который имеет следующее количество элементов со своей взаимосвязью сущностей:

Owner can have many Cars.
Cars only have one Owner.

Может создавать и просматривать владельцев через мой веб-сервис REST.

Каждый раз, когда я пытаюсь создать автомобиль с ассоциированным владельцем, он правильно заполняет строку базы данных, но кажется, что он повторяется бесконечно, вызывает ошибку переполнения стека (см. ниже).


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

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.
}

OwnerRepository:

@Repository
public interface OwnerRepository extends JpaRepository<Owner, Long> {
}

CarRepository:

@Repository
public interface CarRepository extends JpaRepository<Car, Long> {
}

OwnerService:

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

CarService:

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

}

CarController:

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


Принимая во внимание, что я могу создать новые владельцы (и просматривать их в базе данных, а также просматривать их, вызывая getAllOwners через curl / Postman), передавая это как тело запроса:

{
    "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, например так:

---------------------------------------
|id | make  | model  | year | owner_id|
---------------------------------------  
|1  | Honda | Accord | 2020 |     1   |
---------------------------------------  

Есть ли что-то, что я делаю внутри CarServiceImpl.createCar() method это вызывает би -направленное отношение к разрыву?

Создает переполнение стека: null execption:

-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.StackOverflowError] with root cause
java.lang.StackOverflowError: 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)

Удивительно, что несмотря на то, что это трассировка стека, которая появляется каждый раз, когда я создайте новый автомобиль, в базе данных все в порядке (вставка находится внутри таблицы автомобилей с правильным ID владельца внутри строки), и я могу просмотреть полезную нагрузку ответа JSON при выполнении любого из этих запросов GET:

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 и вставки базы данных работают

Ответы [ 2 ]

2 голосов
/ 08 марта 2020

JPA не имеет ничего общего с этой ошибкой. Посмотрите на трассировку стека - ваш Car#toString() печатает его Owner. В то время как Owner#toString() печатает свою коллекцию Cars.

Так что, когда что-то в вашем коде вызывает toString() для одного из этих объектов - это вызывает бесконечную цепочку вызовов, которая заканчивается только тогда, когда максимальная глубина стека потока достигается, вызывая StackOverflowError.

Обычно в toString() мы хотим печатать только примитивы / ValueObjects из текущего класса. Если мы также начнем печатать связанные сущности - это приведет к инициализации ленивых полей.

0 голосов
/ 08 марта 2020

Внутри моего класса Owner вот что 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 +
        '}';
}

Работало!

...