简体   繁体   中英

JPA not clearing a Set iterator when saving in OneToMany relationship

I have a basic SpringBoot app. using Spring Initializer, JPA, embedded Tomcat, Thymeleaf template engine, and package as an executable JAR file.

I have this domain class:

    Entity
    @DiscriminatorValue("sebloc")
    public class SeblocDevice extends Device {

        /**
         * 
         */
        private static final long serialVersionUID = 1L;


        public SeblocDevice() {
            super();
        }

        public SeblocDevice(String deviceKey, String devicePAC) {
            super(deviceKey, devicePAC);
        }

        @OneToMany(mappedBy = "device", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
        private Set<DeviceDriver> driverDevices = new HashSet<>();

        public Set<DeviceDriver> getDriverDevices() {
            return driverDevices;
        }

        public void setDriverDevices(Set<DeviceDriver> driverDevices) {
            this.driverDevices = driverDevices;
        }

      public void clearDriverDevices()  {
    for (DeviceDriver deviceDriver : deviceDrivers) {
        deviceDriver.setDriver(null);
        driverDevices.remove(deviceDriver);
    }

public void removeDriverDevice(DeviceDriver deviceDriver)  {
    deviceDriver.setDriver(null);
    driverDevices.remove(deviceDriver);
}
}
    ...
    }

and this other domain object

@Entity
@Table(name = "t_device_driver")
public class DeviceDriver implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    public DeviceDriver() {

    }

    public DeviceDriver (SeblocDevice device, Driver driver) {
        this.device = device;
        this.driver = driver;
    }


    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "device_id")
    private SeblocDevice device;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "driver_id")
    private Driver driver;

    public SeblocDevice getDevice() {
        return device;
    }

    public void setDevice(SeblocDevice device) {
        this.device = device;
    }

    public Driver getDriver() {
        return driver;
    }

    public void setDriver(Driver driver) {
        this.driver = driver;
    }
}

and this JUnit test, where in the last test I was excepting 1 driver but I got 2 (clear all the drivers, and add 1)

    @Test
    public void testUpdateAuthorizedDriver() {

        SeblocDevice seblocDevice = (SeblocDevice) deviceService.findById((long)1);

        assertEquals (1,seblocDevice.getDriverDevices().size());

        Driver authorizedDriver   = (Driver) driverService.findById((long)2);

        DeviceDriver dd  = new DeviceDriver (seblocDevice, authorizedDriver);

DeviceDriver ddToRemove = seblocDevice.getDeviceDrivers().iterator().next();

        seblocDevice.removeDriverDevice(ddToRemove);

        seblocDevice.clearDriverDevices()
        seblocDevice.getDriverDevices().clear();
        seblocDevice.getDriverDevices().add(dd);

        deviceService.save(seblocDevice);

        assertEquals (1,    seblocDevice.getDriverDevices().size());
        assertEquals (1,    Iterators.size(deviceService.findSeblocDeviceAll().iterator()));

        SeblocDevice seblocDeviceRetrieved  = deviceService.findSeblocDeviceAll().iterator().next();

        assertEquals (1,    seblocDeviceRetrieved.getDriverDevices().size());

    }

I also tried to create a method in the service level

public interface DeviceDriverRepository extends CrudRepository<DeviceDriver, Long> {

}
@Transactional
     public SeblocDevice cleanDrivers (SeblocDevice seblocDevice) {
         deviceDriverRepository.delete(seblocDevice.getDeviceDrivers());
     seblocDevice.getDeviceDrivers().clear();
     seblocDevice.setDeviceDrivers(null);
     return seblocDeviceRepository.save (seblocDevice);
     }

and then deviceService.cleanDrivers(seblocDevice);

but the drivers appears again

crizzis is right you have to set device to null.

The best way to keep a bidirectional association consistent is to create convenience methods like:

public void addDriverDevice(DeviceDriver deviceDriver)  {
    deviceDriver.setDriver(deviceDriver);
    driverDevices.add(deviceDriver);
}

public void removeDriverDevice(DeviceDriver deviceDriver)  {
    deviceDriver.setDriver(null);
    driverDevices.remove(deviceDriver);
}

And if you want to clear all

public void clearDriverDevices()  {
    for (DeviceDriver deviceDriver : deviceDrivers) {
        deviceDriver.setDriver(null);
        driverDevices.remove(deviceDriver);
    }
}

For your code to work as you expect, you need to add the orphanRemoval=true parameter in @OneToMany relationship in SeblocDevice.driverDevices attribute as shown below:

@OneToMany(mappedBy = "device", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
    private Set<DeviceDriver> driverDevices = new HashSet<>();

To clearly understand the JPA mapping. You need to keep in mind that in a relationship, there is always the owner side. For example, in an @OneToMany vs. @ManyToOne relationship, @ManyToOne is the owner because it has the reference to the other entity.

Basically the Entity Manager only cares for changes on the owner side, ie if you invoke DeviceDriver.setDevice(null) , the removal will be performed. But the opposite ( SeblocDevice.getDriverDevices().clear() ) is not true.

For this reason, there is the orphanRemoval parameter, which is self explanatory. When this parameter is assigned, the Entity Manager will now control the elements of the collection as an owner, and a SeblocDevice.getDriverDevices().clear() will remove the DeviceDrivers in database that are not in the SeblocDevice.getDriverDevices collection, even though DeviceDriver.device is not null.

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