JPA: zapisywanie zagnieżdżonych encji w encji

Odpowiedz Nowy wątek
2018-12-04 14:28
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/trolololol[...]rollers/DriverController.java

edytowany 3x, ostatnio: Julian_, 2018-12-04 14:38
Nazwa projektu, piękna :-) chociaż jedno t się zgubiło. - jarekr000000 2018-12-04 15:53

Pozostało 580 znaków

2018-12-04 14:36
eL
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/hib[...]/hibernate-jpa-cascade-types/

sory, pomyłka, już wkleiłem VehicleController... - Julian_ 2018-12-04 14:38
Sprawdź i daj znać czy działa. - eL 2018-12-04 14:40

Pozostało 580 znaków

2018-12-04 14:45
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/hib[...]/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...

edytowany 2x, ostatnio: Julian_, 2018-12-04 14:47
Pokaż pozostałe 2 komentarze
wszystko zwraca tak samo za wyjątkiem pesela (który zwraca zaszyfrowany): { "id": 1, "pesel": "240028313144621540785598931", "firstName": "Janusz", "lastName": "Nosacz", "login": "user2", "email": "[email protected]", "active": true } ale chyba nie musi być to samo? nie wystarczy, że będzie to samo id? Z resztą próbowałem już dać oryginalny pesel i też nie hulało... próbowałem też dorabiać metodę equals tylko na id - Julian_ 2018-12-04 14:53
A to co znaczy? Vehicle widzi Driver, ale Driver nie widzi Vehicle, może dlatego. - eL 2018-12-04 15:01
że mają unidirectional assocation - Julian_ 2018-12-04 15:03
Aaa ok widzę, masz jednokierunkową tę relację. Musiałbym to spullować i zobaczyć więc potrzebuję chwilę. - eL 2018-12-04 15:07
dobra, teraz mozesz pulować ;) - Julian_ 2018-12-04 15:19

Pozostało 580 znaków

2018-12-04 16:10
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).


Bardzo lubie Singletony, dlatego robię po kilka instancji każdego.
edytowany 8x, ostatnio: jarekr000000, 2018-12-04 16:40

Pozostało 580 znaków

2018-12-04 17:23
1

Kilka groszy ode mnie:

  1. W createNewVehicle wywołuesz serwis żeby pobrać Drivera z bazy danych.

    dto.setDriverDto(drvService.getDriver(dto.getDriverDto().getId()));
  2. W serwisie wywołujesz motodę repo i konwertujesz Driver na DriverDto

    return (DriverDto) driverMapper.sourceToDto(driverRepo.findById(id).orElseThrow(ResourceNotFoundException::new));
  3. 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/que[...]sert-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.

edytowany 1x, ostatnio: Seti87, 2018-12-04 17:24
Pokaż pozostałe 2 komentarze
a macie jakies repozytoria projektów na githubie z takim robieniem dto? - Julian_ 2018-12-04 19:42
Tutaj mniej więcej jest to co mam na myśli: Jak poradzić sobie z nullami przy tworzeniu dto? - Seti87 2018-12-04 19:45
a session hibernatowe robić normalnie xmlem czy jakoś przez @Query ? - Julian_ 2018-12-04 19:46
Ze spring data to pewnie @Query będzie najszybciej - Seti87 2018-12-04 20:15
a mapowanie dtoToSource proponujesz wywalić wobec czego użytkownik ma podawać Source a nie Dto jak chce np. zapisać albo apdejtować? - Julian_ 2018-12-05 20:33

Pozostało 580 znaków

2018-12-05 20:35
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;
    }

Pozostało 580 znaków

2018-12-06 20:09
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

edytowany 1x, ostatnio: Julian_, 2018-12-06 20:12

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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