简体   繁体   中英

Spring JPA Entities are losing Data when Adding Same Items to Other Entities

Using Java 1.8, Spring Boot, JPA, I create a Spring Boot Microservice, where the data model (entity relationship) follows this particular one to many relationship:

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

This Spring Boot Microservice has the following functionality:

HTTP GET Endpoints :

  • Obtain data about a particular Owner (name, address, etc.) from database.
  • Retrieve information about a particular Owner's car (make, model etc.) from database.

HTTP POST Endpoints :

  • Persist data about a Owner into database.
  • Persist data about a Owner's Car into database.

These all work when I run the Spring Boot Microservice and manually create Owners & their Cars and also, retrieve them using my GET method endpoints.

What I am trying to do now is to have these be populated when the Spring Boot Microservice loads up (that way, I can start writing unit and integration tests before the Maven build completes).

So, for this I created the following file:


@Component
public class DataInserter implements ApplicationListener<ContextRefreshedEvent> {


    @Value("classpath:data/owners.json")
    Resource ownersResource;

    @Value("classpath:data/cars.json")
    Resource carsResource;

    @Autowired
    private OwnerService ownerService;

    @Autowired
    private CarsService carService;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        List<Owner> populatedOwners = new ArrayList<>();
        try {
            Owner aOwner;

            File ownersFile = ownersResource.getFile();
            File carsFile = carsResource.getFile();

            String ownersString = new String(Files.readAllBytes(ownersFile.toPath()));
            String carsString = new String(Files.readAllBytes(carsFile.toPath()));

            ObjectMapper mapper = new ObjectMapper();
            List<Owner> owners = Arrays.asList(mapper.readValue(ownersString, Owner[].class));
            List<ElectricCars> cars = Arrays.asList(mapper.readValue(carsString, ElectricCars[].class));

            // Populate owners one by one
            for (Owner owner : owners) {
                aOwner = new Owner(owner.getName(), owner.getAddress(), owner.getCity(), owner.getState(), owner.getZipCode());
                ownerService.createOwner(aOwner);
                populatedOwners.add(aOwner);
            }

            // Populate owner cars one by one
            for (int i = 0; i < populatedOwners.size(); i++) {
                carService.createCars(populatedOwners.get(i).getId(), cars.get(i));
            }

            // Provide some owners with multiple cars
 //           carService.createCars(populatedOwners.get(0).getId(), cars.get(3));
 //           carService.createCars(populatedOwners.get(0).getId(), cars.get(4));
 //           carService.createCars(populatedOwners.get(1).getId(), cars.get(3));
        }
        catch(IOException ioe) {
            ioe.printStackTrace();;
        }
    }
}

src/main/resources/data/cars.json:

[
  {
      "make": "Honda",
      "model": "Accord",
      "year": "2020"
  },
  {
      "make": "Nissan",
      "model": "Maxima",
      "year": "2019"
  },
  {
      "make": "Toyota",
      "model": "Prius",
      "year": "2015"
  },
  {
      "make": "Porsche",
      "model": "911",
      "year": "2017"
  },
  {
      "make": "Hyundai",
      "model": "Elantra",
      "year": "2018"
  },
  {
      "make": "Volkswagen",
      "model": "Beatle",
      "year": "1973"
  },
  {
      "make": "Ford",
      "model": "F-150",
      "year": "2010"
  },
  {
      "make": "Chevrolet",
      "model": "Silverado",
      "year": "2020"
  },
  {
      "make": "Toyota",
      "model": "Camary",
      "year": "2018"
  },
  {
      "make": "Alfa",
      "model": "Romeo",
      "year": "2017"
  }
]

src/main/resources/data/owners.json:

[
  {
    "name": "Tom Brady"
  },
  {
    "name": "Kobe Bryant"
  },
  {
    "name": "Mike Tyson"
  },
  {
    "name": "Scottie Pippen"
  },
  {
    "name": "John Madden"
  },
  {
    "name": "Arnold Palmer"
  },
  {
    "name": "Tiger Woods"
  },
  {
    "name": "Magic Johnson"
  },
  {
    "name": "George Foreman"
  },
  {
    "name": "Charles Barkley"
  }

]

So, when I run this with the following lines commented out:

    // Populate owner cars one by one
    for (int i = 0; i < populatedOwners.size(); i++) {
        carService.createCars(populatedOwners.get(i).getId(), cars.get(i));
    }

    // Provide some owners with multiple cars
 // carService.createCars(populatedOwners.get(0).getId(), cars.get(3));
 // carService.createCars(populatedOwners.get(0).getId(), cars.get(4));
 // carService.createCars(populatedOwners.get(1).getId(), cars.get(3));

And then I call my Get All Owners REST Endpoint (see below):

GET http://localhost:8080/car-api/owners

JSON Payload yields correctly (each individual owner has a single car):

[
    {
        "id": 1,
        "name": "Tom Brady",
        "cars": [
            {
                "id": 1,
                "make": "Honda",
                "model": "Accord",
                "year": "2020"
            }
        ]
    },
    {
        "id": 2,
        "name": "Kobe Bryant",
        "cars": [
             {
                "id": 2,
                "make": "Nissan",
                "model": "Maxima",
                "year": "2019"
            }
        ]
    },
    {
        "id": 3,
        "name": "Mike Tyson",
        "cars": [
            {
                "id": 3,
                "make": "Toyota",
                "model": "Prius",
                "year": "2015"
            }
        ]
    },
    {
        "id": 4,
        "name": "Scottie Pippen",
        "cars": [
            {
                "id": 4,
                "make": "Porsche",
                "model": "911",
                "year": "2017"
            }
        ]
    },
    {
        "id": 5,
        "name": "John Madden",
        "cars": [
            {
                "id": 5,
                "make": "Hyundai",
                "model": "Elantra",
                "year": "2018"
            }
        ]
    },
    {
        "id": 6,
        "name": "Arnold Palmer",
        "cars": [
            {
                "id": 6,          
                "make": "Volkswagen",
                "model": "Beatle",
                "year": "1973"
            }
        ]
    },
    {
        "id": 7,
        "name": "Tiger Woods",
        "cars": [
            {
                "id": 7,
                "make": "Ford",
                "model": "F-150",
                "year": "2010"
            }
        ]
    },
    {
        "id": 8,
        "name": "Magic Johnson",
        "cars": [
            {
                "id": 8,
                "make": "Chevrolet",
                "model": "Silverado",
                "year": "2020"
            }
        ]
    },
    {
        "id": 9,
        "name": "George Foreman",
        "cars": [
            {
                "id": 9,
                "make": "Toyota",
                "model": "Camary",
                "year": "2018"
            }
        ]
    },
    {
        "id": 10,
        "name": "Charles Barkley",
        "cars": [
            {
                "id": 10,
                "make": "Alfa",
                "model": "Romeo",
                "year": "2017"
            }    
        ]
    }
]

However, when I try to assign more cars to individual owners (it seems this this causes other owner's cars JSON array to become empty):

// Populate owner cars one by one
for (int i = 0; i < populatedOwners.size(); i++) {
    carService.createCars(populatedOwners.get(i).getId(), cars.get(i));
}

// Provide some owners with multiple cars
carService.createCars(populatedOwners.get(0).getId(), cars.get(3));
carService.createCars(populatedOwners.get(0).getId(), cars.get(4));
carService.createCars(populatedOwners.get(1).getId(), cars.get(3));

JSON Payload yield the following:

[
    {
        "id": 1,
        "name": "Tom Brady",
        "cars": [
            {
                "id": 1,
                "make": "Honda",
                "model": "Accord",
                "year": "2020"
            },
            {
                "id": 5,
                "make": "Hyundai",
                "model": "Elantra",
                "year": "2018"
            }
        ]
    },
    {
        "id": 2,
        "name": "Kobe Bryant",
        "cars": [
             {
                "id": 2,
                "make": "Nissan",
                "model": "Maxima",
                "year": "2019"
            },
            {
            {
                "id": 4,
                "make": "Porsche",
                "model": "911",
                "year": "2017"
            }


        ]
    },
    {
        "id": 3,
        "name": "Mike Tyson",
        "cars": [
            {
                "id": 3,
                "make": "Toyota",
                "model": "Prius",
                "year": "2015"
            }
        ]
    },
    {
        "id": 4,
        "name": "Scottie Pippen",
        "cars": []
    },
    {
        "id": 5,
        "name": "John Madden",
        "cars": []
    },
    {
        "id": 6,
        "name": "Arnold Palmer",
        "cars": [
            {
                "id": 6,          
                "make": "Volkswagen",
                "model": "Beatle",
                "year": "1973"
            }
        ]
    },
    {
        "id": 7,
        "name": "Tiger Woods",
        "cars": [
            {
                "id": 7,
                "make": "Ford",
                "model": "F-150",
                "year": "2010"
            }
        ]
    },
    {
        "id": 8,
        "name": "Magic Johnson",
        "cars": [
            {
                "id": 8,
                "make": "Chevrolet",
                "model": "Silverado",
                "year": "2020"
            }
        ]
    },
    {
        "id": 9,
        "name": "George Foreman",
        "cars": [
            {
                "id": 9,
                "make": "Toyota",
                "model": "Camary",
                "year": "2018"
            }
        ]
    },
    {
        "id": 10,
        "name": "Charles Barkley",
        "cars": [
            {
                "id": 10,
                "make": "Alfa",
                "model": "Romeo",
                "year": "2017"
            }    
        ]
    }
]

As you can see, it seems like these cars were added to Tom Brady and Kobey Bryant's JSON array of cars but removed from the people who had them (Scottie Pippen & John Madden now have empty JSON arrays of cars)...

Why is this happening, is this a possible bug with my CarServiceImpl.createCar() method?


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=

Owner entity:

@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.EAGER,
                mappedBy = "owner")
    private List<Car> cars = new ArrayList<>();

    public Owner() {
    }

    // Getter & Setters omitted for brevity.
}

Car entity:

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

}


OwnerController:

@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 CarController() {
        headers = new HttpHeaders();
        headers.add("Content-Type", "application/json");
    }

    @RequestMapping(value = { "/cars/{ownerId}" }, method = RequestMethod.POST, produces = "APPLICATION/JSON")
    public ResponseEntity<Object> createCarBasedOnOwnerId(@Valid @RequestBody Car car, Long ownerId) {
        boolean isCreated = carService.createCar(ownerId, car);
        if (isCreated) {
            return new ResponseEntity<Object>(headers, HttpStatus.OK);
        }
        else {
            return new ResponseEntity<Object>(HttpStatus.NOT_FOUND);
        }
    }


Question(s):

  1. Why by adding new cars to an Owner's car ArrayList, it removes other Owner's cars (which have the same car.id)?

  2. Noticed how inside Owner.java, I had to make the FetchType.EAGER :

@OneToMany(cascade = CascadeType.ALL,
           fetch = FetchType.EAGER,
           mappedBy = "owner")
private List<Car> cars = new ArrayList<>();


When I had it as fetch = FetchType.LAZY it threw the following Exception:

2020-03-08 15:18:13,175 ERROR org.springframework.boot.SpringApplication [main] Application run failed
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.myapi.model.User.cars, could not initialize proxy - no Session
        at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:606)
        at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218)
        at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:585)
        at org.hibernate.collection.internal.AbstractPersistentCollection.write(AbstractPersistentCollection.java:409)
        at org.hibernate.collection.internal.PersistentBag.add(PersistentBag.java:407)
        at org.hibernate.collection.internal.PersistentBag.add(PersistentBag.java:407)
        at com.myapi.service.CarServiceImpl.createCar(CarServiceImpl.java:36)
        at com.myapi.bootstrap.DataInserter.onApplicationEvent(DataInserter.java:71)
        at com.myapi.bootstrap.DataInserter.onApplicationEvent(DataInserter.java:24)
        at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
        at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
        at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
        at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:403)
        at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360)
        at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:897)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:162)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:553)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:747)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215)
        at com.myapi.CarApplication.main(CarApplication.java:12)


Is this related or a separate issue altogether? Am somewhat new to JPA so am wondering if I need to change the values for cascade = CascadeType.ALL in both entities to something else.

  1. Is there a better way to populate the database with mock data (perhaps in the unit or integration tests rather than on ApplicationContext load up) for testing purposes?
  1. Why by adding new cars to an Owner's car ArrayList, it removes other Owner's cars (which have the same car.id)?

Because you've programmed it so. That's how you defined the relationship between the car and the owner:

Cars only have one Owner.

So how do you want the car to have multiple owners? If you want to have the same car multiple times, you would have to create a new entity with the same data (except for id).

2. LazyInitializationException

A X-to-many relationship in hibernate (one-to-many, many-to-many) is always fetched lazily. This means, that when you're fetching an entity with to-many relationship, for performance reasons the collection is not fetched. If you try to iterate over it, LazyInitializationException is thrown. Annotating with FetchType.EAGER is a solution but not a good one, since the collection will always be fetched no matter if it's needed. A better one would be to use for example jpql in your repository:

@Query("select o from Owner o where o.id = :id left join fetch o.cars")
findOrderWithCars(@Param("id") Long ownerId)
  1. Is there a better way to populate the database with mock data (perhaps in the unit or integration tests rather than on ApplicationContext load up) for testing purposes?

Yes. Such a solution would be for example using flyway . You only need to create sql scripts, that populate the database with data and configure the datasource. You won't have to write that much code, map json objects etc.


Side note: this code is a killer:

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

First, you're checking if the entity exists in the database and if so, you're firing another shot to the database to fetch it. It can be done in single roundtrip. Another this is you're checking if Optional is null . Optional should never be null . You probably wanted to write owner.isPresent() .

The issue with DataInserter is it is using the same car objects to re-assigned it to different owners. If we want to assign multiple cars to owners we need to have more car objects than owners. We need to clone the car objects to create different car objects with the same properties.

We can write the copy constructor in Car Class that takes the existing Car and returns the copy or clone of the car with the same properties. So that we can create a new car object with existing car objects. For example, as shown below

     Car newCarObject  = new Car(existingCarObject);

And modify the below code to create a new car object with existing car objects.

  // Provide some owners with multiple cars
  // carService.createCars(populatedOwners.get(0).getId(), new Car(cars.get(3)));
  // carService.createCars(populatedOwners.get(0).getId(), new Car(cars.get(4)));
  // carService.createCars(populatedOwners.get(1).getId(), new Car(cars.get(3)));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM