3

I have a OneToMany relationship (two tables bi-directional). When I save the doctor's specialties, it does work, but when I remove any specialty and update the doctor, it doesn't.

Doctor

@Entity
@Table(name = "doctors")
public class Doctor implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer doctorId;

    @Column(length = 20)
    private String doctorName;

    @Column(length = 9)
    private String doctorPhoneNo;

    @Column(length = 30)
    private String doctorEmailAddress;

    private String doctorProfileImage;

    @Enumerated(EnumType.STRING)
    private Status status;

    @Column(length = 6)
    private String doctorCmp;

    @OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL)
//  @JsonIgnore
    private Set<DoctorSpecialties> doctorSpecialties;

    public Doctor() {
        this.doctorSpecialties = new HashSet<>();
    }

    public Doctor(Integer id){
        this();
        this.doctorId = id;
    }

    // getters y setters
}

Specialty

@Entity
@Table(name = "specialties")
public class Specialty implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer specialtyId;

    private String specialtyName;

    @OneToMany(mappedBy = "specialty")
    @JsonIgnore
    private Set<DoctorSpecialties> doctorSpecialties;

    public Specialty() {
    }

    public Specialty(Integer id) {
        this.specialtyId = id;
    }

    // getters and setters
  }

DoctorSpecialties

@Entity
@Table(name = "doctor_specialties")
public class DoctorSpecialties implements Serializable {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "doctor_id")
    private Doctor doctor;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "specialty_id")
    private Specialty specialty;

    @OneToMany
    @JoinColumn(name = "doctor_specialties_id")
    private Set<Appointment> appointments;

    @OneToMany
    @JoinColumn(name = "doctor_specialties_id")
    private Set<DoctorSchedule> schedules;

    public DoctorSpecialties(){

    }

    public DoctorSpecialties(Specialty specialty, Doctor doctor){
        this.specialty = specialty;
        this.doctor = doctor;
    }
      getters / setters
 }

Controller

@PostMapping(value = "/saveSpecialties/{id}")
public String saveDoctorSpecialties(@RequestParam(required = false) String[] specialtiesId,
                                    @PathVariable Integer id, RedirectAttributes message) {

    if (id != null && id > 0) {
        Doctor doctor = doctorService.findOne(id);

        if (doctor != null) {

            // It does not work
            doctor.getDoctorSpecialties().forEach(ds -> doctorSpecialtiesService.delete(ds.getId()));

            doctor.getDoctorSpecialties().clear();

            if (specialtiesId != null) {
                for (String specialtyId : specialtiesId) {
                    DoctorSpecialties ds = new DoctorSpecialties();
                    ds.setSpecialty(new Specialty(Integer.parseInt(specialtyId)));
                       
                    ds.setDoctor(doctor);

                    doctor.getDoctorSpecialties()
                            .add(ds);
                }
                
            }
            doctorService.update(doctor);
            message.addFlashAttribute("success", "Specialties successfully saved.");
            return "redirect:/doctors/profile/{id}/specialties";
        }
    }
    // specialtiesId = new String[]{};
    message.addFlashAttribute("error", "Doctor doesn't exists");
    return "redirect:/doctors/list";
}

console:

2021-10-30 21:19:13.330 DEBUG 44504 --- [nio-8080-exec-7] org.hibernate.SQL : select doctor0_.doctor_id as doctor_i1_3_0_, doctor0_.doctor_cmp as doctor_c2_3_0_, doctor0_.doctor_email_address as doctor_e3_3_0_, doctor0_.doctor_name as doctor_n4_3_0_, doctor0_.doctor_phone_no as doctor_p5_3_0_, doctor0_.doctor_profile_image as doctor_p6_3_0_, doctor0_.status as status7_3_0_ from doctors doctor0_ where doctor0_.doctor_id=? 2021-10-30 21:19:13.339 DEBUG 44504 --- [nio-8080-exec-7] org.hibernate.SQL : select doctorspec0_.doctor_id as doctor_i2_2_0_, doctorspec0_.id as id1_2_0_, doctorspec0_.id as id1_2_1_, doctorspec0_.doctor_id as doctor_i2_2_1_, doctorspec0_.specialty_id as specialt3_2_1_ from doctor_specialties doctorspec0_ where doctorspec0_.doctor_id=? 2021-10-30 21:19:13.401 DEBUG 44504 --- [nio-8080-exec-8] org.hibernate.SQL : select doctor0_.doctor_id as doctor_i1_3_0_, doctor0_.doctor_cmp as doctor_c2_3_0_, doctor0_.doctor_email_address as doctor_e3_3_0_, doctor0_.doctor_name as doctor_n4_3_0_, doctor0_.doctor_phone_no as doctor_p5_3_0_, doctor0_.doctor_profile_image as doctor_p6_3_0_, doctor0_.status as status7_3_0_ from doctors doctor0_ where doctor0_.doctor_id=? 2021-10-30 21:19:13.404 DEBUG 44504 --- [nio-8080-exec-8] org.hibernate.SQL : select specialty0_.specialty_id as specialt1_7_0_, doctorspec1_.id as id1_2_1_, doctor2_.doctor_id as doctor_i1_3_2_, specialty0_.specialty_name as specialt2_7_0_, doctorspec1_.doctor_id as doctor_i2_2_1_, doctorspec1_.specialty_id as specialt3_2_1_, doctorspec1_.specialty_id as specialt3_2_0__, doctorspec1_.id as id1_2_0__, doctor2_.doctor_cmp as doctor_c2_3_2_, doctor2_.doctor_email_address as doctor_e3_3_2_, doctor2_.doctor_name as doctor_n4_3_2_, doctor2_.doctor_phone_no as doctor_p5_3_2_, doctor2_.doctor_profile_image as doctor_p6_3_2_, doctor2_.status as status7_3_2_ from specialties specialty0_ inner join doctor_specialties doctorspec1_ on specialty0_.specialty_id=doctorspec1_.specialty_id inner join doctors doctor2_ on doctorspec1_.doctor_id=doctor2_.doctor_id where doctor2_.doctor_id=? 2021-10-30 21:19:13.565 DEBUG 44504 --- [nio-8080-exec-4] org.hibernate.SQL : select specialty0_.specialty_id as specialt1_7_0_, doctorspec1_.id as id1_2_1_, doctor2_.doctor_id as doctor_i1_3_2_, specialty0_.specialty_name as specialt2_7_0_, doctorspec1_.doctor_id as doctor_i2_2_1_, doctorspec1_.specialty_id as specialt3_2_1_, doctorspec1_.specialty_id as specialt3_2_0__, doctorspec1_.id as id1_2_0__, doctor2_.doctor_cmp as doctor_c2_3_2_, doctor2_.doctor_email_address as doctor_e3_3_2_, doctor2_.doctor_name as doctor_n4_3_2_, doctor2_.doctor_phone_no as doctor_p5_3_2_, doctor2_.doctor_profile_image as doctor_p6_3_2_, doctor2_.status as status7_3_2_ from specialties specialty0_ inner join doctor_specialties doctorspec1_ on specialty0_.specialty_id=doctorspec1_.specialty_id inner join doctors doctor2_ on doctorspec1_.doctor_id=doctor2_.doctor_id where doctor2_.doctor_id=?

There is no delete statement...

------------------------ EDIT 1 ------------------------

Doctor find = doctorRepository.findById(1).get();
    
DoctorSpecialties ds1 = new DoctorSpecialties();
ds1.setSpecialty(specialtyRepository.findById(1).get());
ds1.setDoctor(find);
    
DoctorSpecialties ds2 = new DoctorSpecialties();
ds2.setSpecialty(specialtyRepository.findById(2).get());
ds2.setDoctor(find);
    
find.getDoctorSpecialties().add(ds1);
find.getDoctorSpecialties().add(ds2);

doctorRepository.save(find);

I have done some tests and I can not understand completely. I did and it only saves once when actually I am adding two objects.

insert into doctor_specialties (id, doctor_id, specialty_id) values (null, ?, ?)

------------------------ EDIT 2 ------------------------

DoctorSpecialties (Modify the constructor)

@Entity
@Table(name = "doctor_specialties")
public class DoctorSpecialties implements Serializable {

    public DoctorSpecialties(Integer specialtyId, Doctor doctor) {
        this.specialty = new Specialty(specialtyId);
        this.doctor = doctor;
    }

 }

Controller

@PostMapping(value = "/saveSpecialties/{id}")
public String saveDoctorSpecialties(@RequestParam(required = false) String[] specialtiesId,
                                    @PathVariable Integer id, RedirectAttributes message) {
    if (id != null && id > 0) {

        doctorService.saveDelete(id);

        Doctor doctor = doctorService.findOne(id);
        if (specialtiesId != null && specialtiesId.length > 0) {
            for(String idSpecialty : specialtiesId){
                doctorSpecialtiesService.save(new DoctorSpecialties(Integer.parseInt(idSpecialty), doctor));
            }
        }
        message.addFlashAttribute("success", "Specialties successfully saved.");
        return "redirect:/doctors/profile/{id}/specialties";
    }
    message.addFlashAttribute("error", "Doctor doesn't exists");
    return "redirect:/doctors/list";
}

Service

@Override
@Transactional
public void saveDelete(Integer doctorId) {
    Doctor doctor = this.doctorRepository
            .findById(doctorId).get();
    doctor.getDoctorSpecialties().clear();
}

Console:

select doctor0_.doctor_id as doctor_i1_3_0_, doctor0_.doctor_cmp as doctor_c2_3_0_, doctor0_.doctor_email_address as doctor_e3_3_0_, doctor0_.doctor_name as doctor_n4_3_0_, doctor0_.doctor_phone_no as doctor_p5_3_0_, doctor0_.doctor_profile_image as doctor_p6_3_0_, doctor0_.status as status7_3_0_ from doctors doctor0_ where doctor0_.doctor_id=? select doctorspec0_.doctor_id as doctor_i2_2_0_, doctorspec0_.id as id1_2_0_, doctorspec0_.id as id1_2_1_, doctorspec0_.doctor_id as doctor_i2_2_1_, doctorspec0_.specialty_id as specialt3_2_1_ from doctor_specialties doctorspec0_ where doctorspec0_.doctor_id=?

update appointments set doctor_specialties_id=null where doctor_specialties_id=?

update doctor_schedules set doctor_specialties_id=null where doctor_specialties_id=?

delete from doctor_specialties where id=?

DarkVaderM
  • 97
  • 2
  • 10

2 Answers2

3

For the one-many relationship in a transaction, once you get the parent (Doctor) and loop over its children (DoctorSpecialties) [In other words, once you load entire the parent and its children into Persistent state], you won't be able to delete your DoctorSpecialties directly by its repository.


You could try something like the below example to see it:

@Transactional
public void removeLine(Long doctorId, Long specId) {
    Doctor doctor = this.doctorRepository  // (1)
        .findById(doctorId)
        .orElseThrow(IllegalArgumentException::new);
    this.doctorSpecialtiesRepository.deleteById(specId); // (2)
}

At (1), we load doctor into the persistent state. So here, if fetch = FetchType.EAGER, this means that it will load doctor and its all doctorSpecialties into the persistent state and this cause (2) won't give you any effect.

Otherwise, if fetch = FetchType.LAZY, it only load doctor into the persistent state, and at (2) it will be deleted successfully.

Your case was similar, although fetch = FetchType.LAZY, but you looped over the children by using forEach and that loads doctorSpecialties into the persistent state. That's why you cannot delete them.


Suggestion: Using orphanRemoval = true in your parent entity

@OneToMany(mappedBy = "doctor", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<DoctorSpecialties> dss;

and just simply clear its children in your method (In @Transactional method)

doctor.getDoctorSpecialties().clear();
Linh Vu
  • 736
  • 3
  • 7
  • After executing the `clear()` method, the `save()` method must be executed, right? because without that it does not work. Another question. I have done some tests and I can not understand completely. Could you check the edit please? I have added some tests that I did and it only saves once when actually I am adding two objects. – DarkVaderM Nov 01 '21 at 16:23
  • I assumed that you were using methods annotated with `@Transactional`, right? Because, if you don't use `@Transactional`, with `doctor.getDoctorSpecialties().forEach...` and `fetch=LAZY`, you will get the `LazyInitializationException`. With `@Transactional`, it'll automatically save your **persistent entities** when it commits the transactions. About your test, try debugging after you added ds1, and ds2 to check whether you have exactly 2 ds in your `set`. I suspect that, maybe, although you added 2, but the second one has the same `hashCode` so it isn't added to your `set`. – Linh Vu Nov 01 '21 at 16:57
  • Yes, my fault, these methods had to be overwritten. I did the test again and it seems to work, however, when I remove some specialty and save, other entities that are related to DoctorSpecialities are updated. I have edited my post again (edit 2) – DarkVaderM Nov 01 '21 at 19:00
  • `DoctorSpecialities` has `OneToMany` relation with `appointments` and `doctorSchedules`. You can easily imagine that what will happen with the foreign key in `appointments` that point to the parent `DoctorSpecialities` you're going to remove. So, before the `DoctorSpecialities` is removed, it first clears the foreign keys by updating the foreign keys to `null`. Btw, one suggestion: put all your logic (removing part and adding part) into one service method. – Linh Vu Nov 02 '21 at 00:40
  • I had to use the `clear()` method for the `getAppointments()` and `getSchedules()`.. because `null` it does not work. Where can I read more of these topics? I can't find good information. Could you send me some link please. – DarkVaderM Nov 02 '21 at 02:34
  • you could try adding `@OneToMany(cascade = CascadeType.REMOVE) @JoinColumn(name = "doctor_specialties_id") private Set appointments;`. Once `DoctorSpecialities` is removed, it will remove its `Appointment` as well instead of only updating the foreign key to null. For documentation, I think a series will best fit for you this time, I suggest [the series of Laurentiu Spilca](https://www.youtube.com/watch?v=GVdBxlNfnio&list=PLEocw3gLFc8USLd90a_TicWGiMThDtpOJ&ab_channel=LaurentiuSpilca) – Linh Vu Nov 02 '21 at 03:13
0

Whichever side has mappedBy is owning side.

This means something like: "modifications on this side of the relation are already Mapped By the other side of the relation , so no need to track it here separately in an extra table."

Check this url for more information What is the "owning side" in an ORM mapping?

  • I don't understand.. In my example I save the entity with `mappedBy()` and every doctorSpecialties is selecting doctor .. `ds.setDoctor(doctor)`. Could you give me a code example please? – DarkVaderM Oct 31 '21 at 04:55