【问题标题】:Spring JPA Entities are losing Data when Adding Same Items to Other Entities将相同项目添加到其他实体时,Spring JPA 实体正在丢失数据
【发布时间】:2020-06-20 21:51:31
【问题描述】:

使用 Java 1.8、Spring Boot、JPA,我创建了一个 Spring Boot 微服务, 其中数据模型(实体关系)遵循这种特定的一对多关系:

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

此 Spring Boot 微服务具有以下功能:

HTTP GET 端点

  • 从数据库中获取有关特定所有者的数据(姓名、地址等)。
  • 从数据库中检索有关特定所有者汽车(品牌、型号等)的信息。

HTTP POST 端点

  • 将有关所有者的数据保存到数据库中。
  • 将有关车主的汽车的数据保存到数据库中。

当我运行 Spring Boot 微服务并手动创建 Owners 和他们的 Cars 并使用我的 GET 方法端点检索它们时,这些都可以工作。

我现在要做的是在 Spring Boot 微服务加载时填充这些内容(这样,我可以在 Maven 构建完成之前开始编写单元和集成测试)。

因此,为此我创建了以下文件:


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

]

所以,当我运行它时,以下行被注释掉:

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

然后我调用我的 Get All Owners REST 端点(见下文):

获取http://localhost:8080/car-api/owners

JSON Payload 正确产生(每个车主只有一辆车):

[
    {
        "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"
            }    
        ]
    }
]

但是,当我尝试将更多汽车分配给个人车主时(似乎这会导致其他车主的汽车 JSON 数组变为空):

// 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 有效负载产生以下结果:

[
    {
        "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"
            }    
        ]
    }
]

如您所见,这些汽车似乎已添加到 Tom Brady 和 Kobey Bryant 的汽车 JSON 数组中,但从拥有它们的人中删除(Scottie Pippen 和 John Madden 现在有空的汽车 JSON 数组)...

为什么会发生这种情况,这可能是我的 CarServiceImpl.createCar() 方法的错误吗?


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.EAGER,
                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 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);
        }
    }


问题:

  1. 为什么通过将新车添加到 Owner's car ArrayList 中,它会删除其他 Owner 的汽车(具有相同的 car.id)?

  2. 注意到在 Owner.java 内部,我必须创建 FetchType.EAGER

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


当我将它作为 fetch = FetchType.LAZY 时,它抛出了以下异常:

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)


这完全是相关的还是单独的问题?我对 JPA 有点陌生,所以想知道是否需要将两个实体中 cascade = CascadeType.ALL 的值更改为其他值。

  1. 是否有更好的方法来使用模拟数据填充数据库(可能在单元或集成测试中,而不是在 ApplicationContext 加载时)以进行测试?

【问题讨论】:

标签: java spring database spring-data jpql


【解决方案1】:
  1. 为什么通过将新车添加到 Owner's car ArrayList 中,它会删除其他 Owner 的汽车(具有相同的 car.id)?

因为你已经这样编程了。这就是您定义汽车与车主之间关系的方式:

汽车只有一位车主。

那么,您希望汽车如何拥有多个车主?如果您想多次拥有同一辆车,则必须使用相同的数据(id 除外)创建一个新实体。

2。 LazyInitializationException

hibernate 中的 X 对多关系(一对多、多对多)总是延迟获取。这意味着,当您获取具有一对多关系的实体时,出于性能原因,不会获取集合。如果您尝试对其进行迭代,则会抛出 LazyInitializationException。使用FetchType.EAGER 进行注释是一种解决方案,但不是一个好的解决方案,因为无论是否需要,都会始终获取集合。更好的方法是在您的存储库中使用例如 jpql:

@Query("select o from Owner o where o.id = :id left join fetch o.cars")
findOrderWithCars(@Param("id") Long ownerId)
  1. 是否有更好的方法来使用模拟数据填充数据库(可能在单元或集成测试中,而不是在 ApplicationContext 加载时)以进行测试?

是的。例如,这样的解决方案将使用flyway。您只需要创建 sql 脚本,用数据填充数据库并配置数据源。您不必编写那么多代码、映射 json 对象等。


旁注:这段代码是一个杀手:

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

首先,您正在检查该实体是否存在于数据库中,如果存在,您将再次向数据库射击以获取它。它可以在单次往返中完成。另一个是您正在检查Optional 是否为nullOptional 应该永远不会 null。你可能想写owner.isPresent()

【讨论】:

    【解决方案2】:

    DataInserter 的问题在于它使用相同的汽车对象将其重新分配给不同的所有者。如果我们想将多辆汽车分配给所有者,我们需要拥有比所有者更多的汽车对象。我们需要克隆汽车对象以创建具有相同属性的不同汽车对象。

    我们可以在 Car Class 中编写复制构造函数,它接受现有的 Car 并返回具有相同属性的汽车的副本或克隆。这样我们就可以用现有的汽车对象创建一个新的汽车对象。比如如下图

         Car newCarObject  = new Car(existingCarObject);
    

    并修改下面的代码,用现有的汽车对象创建一个新的汽车对象。

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

    【讨论】:

      猜你喜欢
      • 2019-02-13
      • 1970-01-01
      • 1970-01-01
      • 2013-08-18
      • 1970-01-01
      • 2014-08-30
      • 1970-01-01
      • 2011-12-12
      • 1970-01-01
      相关资源
      最近更新 更多