Problem z CrudRepository: Brak możliwości zapisanie nowej encji zawierającej encje istniejące już w bazie.

0

Cześć. Tworzę Rest Api. Problem występuję gdy chce dodać nowe konto moderatora z istniejącymi już danymi (miasto, województwo, ulica). Wszystko jest okej gdy wszystkie dane nie istnieją jeszcze w bazie.

Tak wygląda mój json:

{
    "id": null,
    "basicInfo": {
        "firstName": "Tomasz",
        "lastName": "Tobiak",
        "pesel": "58635214789",
        "startDate": "2017-01-01"
    },
    "contactInfo": {
        "id": null,
        "email": "[email protected]",
        "phoneNumber": "502037198",
        "address": {
            "houseNamber": 25,
            "apartmentNumber": 20,
            "city": {
                "id": 1,
                "name": "Radzyń Podlaski"
            },
            "street": {
                "id": 1,
                "name": "Betlejemska"
            },
            "zipCode": {
                "id": 1,
                "value": "21-307"
            },
            "voivodeship": {
                "id": 1,
                "name": "lubeelskie"
            }
        }
    },
    "bornInfo": {
        "id": null,
        "bornDate": "1999-02-11",
        "city": {
            "id": 1,
            "name": "Radzyń Podlaski"
        }
    }
}

Treść błędu:
**
"message": "detached entity passed to persist: pl.dn.model.placeInfo.City; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: pl.dn.model.placeInfo.City"**

Modele:

Moderator

@Entity
public class Moderator {

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

    @Embedded
    private BasicInfo basicInfo;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "contact_info_id")
    private ModeratorContactInfo contactInfo;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "born_info_id")
    private ModeratorBornInfo bornInfo;

    public Moderator() {
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public BasicInfo getBasicInfo() {
        return basicInfo;
    }

    public void setBasicInfo(BasicInfo basicInfo) {
        this.basicInfo = basicInfo;
    }

    public ModeratorContactInfo getContactInfo() {
        return contactInfo;
    }

    public void setContactInfo(ModeratorContactInfo contactInfo) {
        this.contactInfo = contactInfo;
    }

    public ModeratorBornInfo getBornInfo() {
        return bornInfo;
    }

    public void setBornInfo(ModeratorBornInfo bornInfo) {
        this.bornInfo = bornInfo;
    }


    @Override
    public String toString() {
        return "Moderator{" +
                "id=" + id +
                ", basicInfo=" + basicInfo +
                ", contactInfo=" + contactInfo +
                ", bornInfo=" + bornInfo +
                '}';
    }
}

ModeratorContactInfo

@Entity
@Table(name = "moderator_contact_info")
public class ModeratorContactInfo {

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

    private String email;

    @Column(name = "phone_number")
    private String phoneNumber;

    @Embedded
    private Address address;

    public ModeratorContactInfo() {
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "ModeratorContactInfo{" +
                "id=" + id +
                ", email='" + email + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                ", address=" + address +
                '}';
    }
}

Address

@Embeddable
public class Address {

    @Column(name = "house_number")
    private long houseNamber;

    @Column(name = "apratment_number")
    private long apartmentNumber;


    @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinColumn(name = "city_id")
    private City city;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "street_id")
    private Street street;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "zip_code_id")
    private ZipCode zipCode;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "voivodeship_id")
    private Voivodeship voivodeship;

    public long getHouseNamber() {
        return houseNamber;
    }

    public void setHouseNamber(long houseNamber) {
        this.houseNamber = houseNamber;
    }

    public long getApartmentNumber() {
        return apartmentNumber;
    }

    public void setApartmentNumber(long apartmentNumber) {
        this.apartmentNumber = apartmentNumber;
    }

    public Street getStreet() {
        return street;
    }

    public void setStreet(Street street) {
        this.street = street;
    }

    public City getCity() {
        return city;
    }

    public void setCity(City city) {
        this.city = city;
    }

    public ZipCode getZipCode() {
        return zipCode;
    }

    public void setZipCode(ZipCode zipCode) {
        this.zipCode = zipCode;
    }

    public Voivodeship getVoivodeship() {
        return voivodeship;
    }

    public void setVoivodeship(Voivodeship voivodeship) {
        this.voivodeship = voivodeship;
    }

    @Override
    public String toString() {
        return "Address{" +
                "houseNamber=" + houseNamber +
                ", apartmentNumber=" + apartmentNumber +
                ", street=" + street +
                '}';
    }
}

City

@Entity
@Table(name = "city")
public class City {

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

    private String name;


    public City() {}

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "City{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Dao do zapisania moderatora, które jest potem wykorzystywane w kontrolerze.

@Repository
public interface ModeratorDao extends CrudRepository<Moderator, Long> {

    public void delete(long id);
    public Moderator save(Moderator moderator);
    public Moderator findById(Long id);

}

Nie mam już pomysłu jakiej implementacji użyć żeby to poszło do bazy.

0

Ach ta encja na twarz.

Wszystko jest napisane w kodzie błędu: city jest odłączone od sesji hibernate.
Musisz je podłączyć (np. przez merge). Rozwiązań jest wiele - prytanie co dokładnie robisz w kodzie, którysię wywala (jak wygląda tówj PUT czy tam POST). Pytanie skąd się wziął ten konkretny obiekt typu City który jest w Address.

0

Odnośnie obiektu City: miałem taki pomysł że jeśli takie miasto znajduje się w bazie - to można by było je wykorzystać i powiązać z moderatorem. Wszystko jest okej gdy json wygląda w ten sposób (wszystkie id są na null):

{
    "id": null,
    "basicInfo": {
        "firstName": "Albert",
        "lastName": "Nurzyński",
        "pesel": "58635214789",
        "startDate": "2017-01-01"
    },
    "contactInfo": {
        "id": null,
        "email": "[email protected]",
        "phoneNumber": "502037198",
        "address": {
            "houseNamber": 25,
            "apartmentNumber": 20,
            "city": {
                "id": null,
                "name": "Radzyń Podlaski"
            },
            "street": {
                "id": null,
                "name": "Betlejemska"
            },
            "zipCode": {
                "id": null,
                "value": "21-307"
            },
            "voivodeship": {
                "id": null,
                "name": "lubeelskie"
            }
        }
    },
    "bornInfo": {
        "id": null,
        "bornDate": "1999-02-11",
        "city": {
            "id": null,
            "name": "Radzyń Podlaski"
        }
    }
}

Wracając do jsona z pierwszego postu: gdy zmienię cascade = CascadeType.ALL na cascade = CascadeType.MERGE wszystko wchodzi do bazy bez problemu.

Tak wygląda mój kontroller:

@RestController
@RequestMapping(value = "moderators")
public class ModeratorManagement {

    @Autowired
    private ModeratorDao moderatorDao;

    @RequestMapping(value = "add", method = RequestMethod.POST)
    public String add(@RequestBody Moderator moderator) {

        return moderatorDao.save(moderator).toString();

    }
}

Myślałem jeszcze nad opracowaniem specjalnego serwisu np:

@Service
public class ModeratorService  {

    @Autowired
    private SessionFactory sessionFactory;

    @Autowired
    private ModeratorDao moderatorDao;

    @Transactional
    public Moderator save(Moderator moderator) {
        Session session = sessionFactory.getCurrentSession();

        session.merge(moderator);
        moderator = moderatorDao.save(moderator);

        return moderator;
    }

}

Ale będę musiał go jeszcze dopracować.

0

Póki co poradziłem sobie w ten sposób: anotacje zostawiłem jak w pierwszym poście i posłużyłem się następującym serwisem:

@Service
public class ModeratorService  {

    @Autowired
    private SessionFactory sessionFactory;

    @Transactional
    public Moderator save(Moderator moderator) {

        Session session = sessionFactory.getCurrentSession();
        session.saveOrUpdate(moderator);

        return moderator;
    }

}

Kontroller przybrał następującą postać:

@RestController
@RequestMapping(value = "moderators")
public class ModeratorManagement {

    @Autowired 
    private ModeratorService moderatorService;

    @RequestMapping(value = "add", method = RequestMethod.POST)
    public String add(@RequestBody Moderator moderator) {

        return moderatorService.save(moderator).toString();

    }

}
1

Wracając do jsona z pierwszego postu: gdy zmienię cascade = CascadeType.ALL na cascade = CascadeType.MERGE wszystko wchodzi do bazy bez problemu.

W zasadzie tutaj sam sobie odpowiedziałeś. A działa dlatego, że :

  1. metoda save() z CrudRepository wykonuje em.persist(), jeśli jest wołana na nowym obiekcie oraz em.merge(), jeśli jest wołana obiekcie z ID.
  2. em.persist() wykonuje się kaskadowo dla wszystkich referencji oznaczonych przez CascadeType.PERSIST lub CascadeType.ALL
  3. encja posiadająca niepusty/niezerowy ID jest domyślnie traktowana nie jako nowa, ale jako odpięta (detached) od EntityManagera.
  4. jeśli em.persist() dostanie odpiętą encję, to rzuca wyjątek.

Zauważ, że w powyższym scenariuszu tylko na poziomie głównej encji masz możliwość wyboru ID. Jeśli tworzysz zupełnie nową encję, to z powodu tej propagacji wszystkie referencje też muszą być nowe. Jeśli natomiast użyjesz CascadeType.MERGE, to zauważ, że em.persist() przestaje propagować się w dół.

Jeśli tworzysz REST-owe API, wydaje mi się, że o wiele wygodniejsze byłoby zamiast ID, używanie adresów URL do ich reprezentacji. Jest nawet fajna konwencja ich zapisu w JSON-ach: http://stateless.co/hal_specification.html . Dużo wygodniej się później operuje takimi obiektami po stronie klienta korzystającego z Twojego API. Zauważ, że w tej konwencji miałbyś jednoznaczną sytuację. Jeśli chcesz stworzyć nowe miasto, po prostu atrybut city zawierałby dane nowego obiektu. A gdybyś chciał podpiąć istniejący, wrzucałbyś tam po prostu niewielki obiekt zawierający tylko URL. A gdybyś chciał dodatkowo ten istniejący zmodyfikować, to wrzucałbyś obiekt, który ma zarówno URL, jak i jakieś atrybuty.

Wymagać to może więcej przetwarzania po stronie backendu (w szczególności musisz napisać kawałek kodu, który rozwiązywałby Ci referencje i ładował potrzebne rzeczy), ale wg mnie gra jest warta świeczki.

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