JPA: zapisywanie zagnieżdżonych encji w encji

0

User jest w Driver.
Driver jest w Vehicle.

Jak chcę zapisać vehicle:

@RestController
@RequestMapping("/api/vehicles")
public class VehicleController {

 // ...
	
	@PostMapping
	@ResponseStatus(code = HttpStatus.CREATED)
	public VehicleDto createNewVehicle(@RequestBody VehicleDto dto) {
		return service.createNewVehicle(dto);
	}
// ...
	}
}
JSON:
{
            "vin": "1G11E5SL6EU150861",
            "mileageKm": 1434,
            "capacityKg": 15300,
            "purchaseDate": [
                2018,
                1,
                17
            ],
            "driverDto": {
                "id": 1
               
            }
        }

to dostaję komunikat błędu:
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.julian.bella.domain.Vehicle.driver -> com.julian.bella.domain.Driver; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.julian.bella.domain.Vehicle.driver -> com.julian.bella.domain.Driver"

no, ale przecież wybrałem Drivera już zapisanego:

@Service
public class VehicleServiceImpl implements VehicleService {


// ...


	@Override
	public VehicleDto saveVehicle(Vehicle vehicle) {
		Vehicle v = repo.save(vehicle);
		return mapper.sourceToDto(v);
	}

	@Override
	public VehicleDto createNewVehicle(VehicleDto dto) {
		System.out.println(dto.getDriverDto());
		dto.setDriverDto(drvService.getDriver(dto.getDriverDto().getId()));
		System.out.println(dto.getDriverDto());
		Vehicle v = mapper.dtoToNewSource(dto);
		return this.saveVehicle(v);
	}

// ...

}

github: https://github.com/trolololololo4/bellaputanesca/blob/master/bella-core/src/main/java/com/julian/bella/controllers/DriverController.java

1

Piszesz że chcesz zapisać vehicle

Jak chcę zapisać vehicle:

a pokazujesz driver controller.
A problem twój pewnie rozwiąże kaskadowość. Dodaj sobie do klasy Vehicle tam gdzie masz relację z Driver cascade=CascadeType.ALL

Tu masz trochę więcej na ten temat: https://howtodoinjava.com/hibernate/hibernate-jpa-cascade-types/

0
eL napisał(a):

Piszesz że chcesz zapisać vehicle

Jak chcę zapisać vehicle:

a pokazujesz driver controller.
A problem twój pewnie rozwiąże kaskadowość. Dodaj sobie do klasy Vehicle tam gdzie masz relację z Driver cascade=CascadeType.ALL

Tu masz trochę więcej na ten temat: https://howtodoinjava.com/hibernate/hibernate-jpa-cascade-types/

cascade nie pomogło... Vehicle widzi Driver, ale Driver nie widzi Vehicle, może dlatego.

@Entity
public class Vehicle {

	@Id
	@VinConstraint
	@Column(length = 17, unique = true, updatable = false)
	public final String vin; // Vehicle Identification Number

// ...
	@OneToOne(cascade=CascadeType.ALL)
	private Driver driver;
@Entity
public class Driver extends Employee {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

// ...
}

Z resztą moim zdaniem, tu nie powinno być żadnej kaskady ALL, bo np. byznesowo usunięcie Vehicle nie powinno powodować usunięcie Driver. Dziwne jest to, że przy tworzeniu Vehicle podaję przecież istniejącego Driver a mimo to JPA zgłasza błąd, że ten Driver jest nie zapisany...

1

Oj @Julian_ .Nieładnie tak kłamać.

no, ale przecież wybrałem Drivera już zapisanego:

Zaglądamy do VehicleMapper.java


	@Override
	public Vehicle dtoToNewSource(VehicleDto dto) {
		if(dto == null) {
			return null;
		}
		Driver drv = (Driver) driverMapper.dtoToNewSource(dto.getDriverDto()); //Jarek: ciekawe co tam jest???
		Vehicle v = new Vehicle(dto.getVin());
		v.setDriver(drv);
		v.setCapacity(dto.getCapacityKg());
		v.setPurchaseDate(dto.getPurchaseDate());
		return v;
	}

Zaglądamy zatem DriverMapper.java, a tam:

@Override
	public Driver dtoToNewSource(DriverDto dto) {
		if(dto == null) {
			return null;
		}
		User user = userMapper.dtoToNewSource(dto.getUserDto());

		Driver source = new Driver(dto.getPesel()); //sourceClass.getDeclaredConstructor(String.class).newInstance(dto.getPesel()); ///Zapisany?? No Panie.....
		
		source.setActive(dto.isActive());
		source.setFirstName(dto.getFirstName());
		source.setLastName(dto.getLastName());
		source.setUser(user);
		return source;
	}

Czyli Driver nie jest zapisany. Bo go właśnie tu stworzyłeś. Przez new.
Formalne to nie jest: managed. Wyrażenie zapisany jest nieprecyzyjne. Musisz ogarnąć problem:* managed vs detached vs transient* (czyli standardowy problem JPA numer 2).

1

Kilka groszy ode mnie:

  1. W createNewVehicle wywołuesz serwis żeby pobrać Drivera z bazy danych.
dto.setDriverDto(drvService.getDriver(dto.getDriverDto().getId()));
  1. W serwisie wywołujesz motodę repo i konwertujesz Driver na DriverDto
return (DriverDto) driverMapper.sourceToDto(driverRepo.findById(id).orElseThrow(ResourceNotFoundException::new));
  1. Po to żeby w tym miejscu z powrotem konwertować DriverDto na Driver:
Vehicle v = mapper.dtoToNewSource(dto);
Driver drv = (Driver) driverMapper.dtoToNewSource(dto.getDriverDto());

W sumie po co to wszystko skoro zamierzasz zainsertwać jeden wpis do bazy danych i wszystko masz już w rękach? Konwertowanie dto tam i z powrotem jest bez sensu. Pobierasz niepotrzebnie obiekt Driver, a przecież wystarczy Ci samo jego id do zapisania Vehicle.

W przypadku mappingu jaki masz powinieneś korzystać z metody "load" zamiast "find". Load tworzy proxy z samym id na potrzeby insertowania obiektu.
Możesz zacząć od zapoznania się z tym tematem:
https://stackoverflow.com/questions/1820458/hibernate-insert-or-update-without-select

Możesz też spróbować innego stylu mapowania - na powiązanych obiektach dajesz " insertbale = false, updatable = false", a sam id mapujesz jako osobna kolumna:

	@Column(name = "driver_id")
	private Integer driverId;

	@OneToOne
	@JoinColumn(name = "driver_id", insertbale = false, updatable = false)
	private Driver driver;

Taki styl sprawdza się lepiej dla nowych osób w JPA, bo nie przychodzą do głowy pomysły w stylu selectowania pól z bazy, gdy tego nie potrzeba.
Id innego drivera ustawiasz po driverId, a w zapytaniach spokojnie możesz odwoływać się poprzez driver.

Ogólnie to całe JPA/Hibernate uważam za bardzo mało intuicyjny framework. Ostatnio jestem zakochany w JOOQ - zastanawiam się czemu nie jest bardziej popularne.

PS. Jeżeli jednak używasz JPA, to pozwolę sobie jeszcze raz polecić pobieranie danych przy pomocy "SELECT new dto(...) FROM" - moim zdaniem najlepsza metoda pobierania danych przy pracy z JPA.

0

Działa, dziękuję za pomoc.

Zamiast tworzyć nową instancję zagnieżdżonych klas w Mapperze zrobiłęm tak:

@Override
	public Driver dtoToNewSource(DriverDto dto) {
		
// ...
		
		User user = null;
		try {
			user = userRepo.findByLogin(dto.getUserDto().getLogin()).get();
		} finally {
			
		}

		Driver source = new Driver(dto.getPesel());
		
// ,,,
		return source;
	}
0

Kolejna rzecz:

chcę dodać możliwość przypisywania Driver do Vehicle. Jak to zrobić zgodnie z dobrymi praktykami?

Podawać ścieżkę do Vehicle w http a w jsonie id Driver?

...api/vehicles/vin=WBAVB33556KS32418/assigndriver

{
"driverDto": {
                "id": 1
            }
}

czy lepiej w http od razu podawać też id Drivera?
...api/vehicles/vin=WBAVB33556KS32418/assigndriver/id=1

1 użytkowników online, w tym zalogowanych: 0, gości: 1