I've looked at a bunch of answers to similar questions and I haven't found one that solves the issue for me. I feel completely lost right now.
I'm developping a Spring Boot web app, using Spring Data JPA for persistence. I get the following error when I try to do an update to an existing entity:
org.hibernate.PersistentObjectException: detached entity passed to persist: com.project.timefit.model.WeeklyRoutinePlan
I'm trying to edit an entity WeeklyRoutinePlan using a crudRepository (the get, create and delete methods work fine).
Here is the method in my service
@Override
@Transactional
public void editRoutinePlan(WeeklyRoutinePlan routinePlan, String username) {
WeeklyRoutinePlan oldRoutinePlan = weeklyRoutinePlanRepository.findById(routinePlan.getId()).get();
oldRoutinePlan.setWeekDay(routinePlan.getWeekDay());
weeklyRoutinePlanRepository.save(oldRoutinePlan);
}
I've simplified it for the sake of debugging, normally i would do a username verification and map the other fields too. I still get the error with this simplified version. And I even get it if I don't change the weekday.
My entity itself looks like this
@Entity
@Data
@SuperBuilder
@NoArgsConstructor
public class WeeklyRoutinePlan{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne
private Routine routine;
private LocalTime startTime;
private LocalTime endTime;
private Integer weekDay;
@ManyToOne
private Program program;
public void setRoutine(Routine routine) {
if(routine == this.routine){
return;
}
if(this.routine != null){
this.routine.getWeeklyRoutinePlans().remove(this);
}
if(!routine.getWeeklyRoutinePlans().contains(this)){
routine.getWeeklyRoutinePlans().add(this);
}
this.routine = routine;
}
public void setProgram(Program program) {
if(program == this.program){
return;
}
if(this.program != null){
this.program.getWeeklyRoutines().remove(this);
}
if(!program.getWeeklyRoutines().contains(this)){
program.getWeeklyRoutines().add(this);
}
this.program = program;
}
}
And here are the related entities (although I'm not sure that's relevant since I'm not even updating the relations)
Routine
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Routine {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne
private User user;
private String name;
private Integer numberOfCycles;
private Color color;
@ManyToMany(mappedBy = "routines")
private List<Exercise> exercises;
@OneToMany(mappedBy = "routine", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<WeeklyRoutinePlan> weeklyRoutinePlans = new ArrayList<>();
@OneToMany(mappedBy = "routine", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<FrequencyRoutinePlan> frequencyRoutinePlans = new ArrayList<>();
@OneToMany(mappedBy = "routine", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<IndividualRoutinePlan> individualRoutinePlans = new ArrayList<>();
public void remove(){
exercises.forEach(exercise -> exercise.getRoutines().remove(this));
exercises = new ArrayList();
}
public void setExercises(List<Exercise> exercises){
exercises.forEach(e -> {
if(e.getRoutines().stream().noneMatch(r -> Objects.equals(r.getId(), this.getId())))
e.getRoutines().add(this);
});
this.exercises = exercises;
}
public void setWeeklyRoutinePlans(List<WeeklyRoutinePlan> weeklyRoutinePlans) {
if(weeklyRoutinePlans == this.weeklyRoutinePlans){
return;
}
this.weeklyRoutinePlans.forEach(r -> {if(!weeklyRoutinePlans.contains(r))r.setRoutine(null);});
weeklyRoutinePlans.forEach(r -> {if(weeklyRoutinePlans.contains(r))r.setRoutine(this);});
this.weeklyRoutinePlans = weeklyRoutinePlans;
}
public void setFrequencyRoutinePlans(List<FrequencyRoutinePlan> frequencyRoutinePlans) {
if(frequencyRoutinePlans == this.frequencyRoutinePlans){
return;
}
this.frequencyRoutinePlans.forEach(r -> {if(!frequencyRoutinePlans.contains(r))r.setRoutine(null);});
frequencyRoutinePlans.forEach(r -> {if(frequencyRoutinePlans.contains(r))r.setRoutine(this);});
this.frequencyRoutinePlans = frequencyRoutinePlans;
}
public void setIndividualRoutinePlans(List<IndividualRoutinePlan> individualRoutinePlans) {
if(individualRoutinePlans == this.individualRoutinePlans){
return;
}
this.individualRoutinePlans.forEach(r -> {if(!individualRoutinePlans.contains(r))r.setRoutine(null);});
individualRoutinePlans.forEach(r -> {if(individualRoutinePlans.contains(r))r.setRoutine(this);});
this.individualRoutinePlans = individualRoutinePlans;
}
@Override
public String toString() {
return "Routine{"
"id=" id
", user=" user.getUsername()
", name='" name '\''
", numberOfCycles=" numberOfCycles
", color=" color
'}';
}
}
Program
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class Program {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private ProgramSetting programSetting = ProgramSetting.WEEKLY;
private Integer frequency = 1;
@OneToMany(mappedBy = "program", cascade = CascadeType.ALL, orphanRemoval = true)
private List<WeeklyRoutinePlan> weeklyRoutines = new ArrayList<>();
@OneToMany(mappedBy = "program", cascade = CascadeType.ALL, orphanRemoval = true)
private List<FrequencyRoutinePlan> frequencyRoutines = new ArrayList<>();
@OneToMany(mappedBy = "program", cascade = CascadeType.ALL, orphanRemoval = true)
private List<IndividualRoutinePlan> individualRoutines = new ArrayList<>();
@ManyToOne
private User user;
public void setWeeklyRoutines(List<WeeklyRoutinePlan> weeklyRoutines) {
if(weeklyRoutines == this.weeklyRoutines){
return;
}
this.weeklyRoutines.forEach(r -> {if(!weeklyRoutines.contains(r))r.setProgram(null);});
weeklyRoutines.forEach(r -> {if(weeklyRoutines.contains(r))r.setProgram(this);});
this.weeklyRoutines = weeklyRoutines;
}
public void setFrequencyRoutines(List<FrequencyRoutinePlan> frequencyRoutines) {
if(frequencyRoutines == this.frequencyRoutines){
return;
}
this.frequencyRoutines.forEach(r -> {if(!frequencyRoutines.contains(r))r.setProgram(null);});
frequencyRoutines.forEach(r -> {if(frequencyRoutines.contains(r))r.setProgram(this);});
this.frequencyRoutines = frequencyRoutines;
}
public void setIndividualRoutines(List<IndividualRoutinePlan> individualRoutines) {
if(individualRoutines == this.individualRoutines){
return;
}
this.individualRoutines.forEach(r -> {if(!individualRoutines.contains(r))r.setProgram(null);});
individualRoutines.forEach(r -> {if(individualRoutines.contains(r))r.setProgram(this);});
this.individualRoutines = individualRoutines;
}
public void setUser(User user) {
this.user = user;
}
@Override
public String toString() {
return "Program{"
"id=" id
", name='" name '\''
", programSetting=" programSetting
", frequency=" frequency
", user=" user.getUsername()
'}';
}
}
I'm a little desperate at this point because my other entities work fine and I just don't get what I'm doing wrong. The method is transactional so the entity shouldn't be detached when I do the save.
I get the same issue if I don't explicitly call save since that's done by the framework anyway.
I've tried setting the fetch on the relations to LAZY, I've also tried EAGER, I've tried having the cascade on Program be only REMOVE, even though best practice is ALL but I'd read some people had issues with PERSIST.
I've made sure my setters update the relations properly even though as I said I've simplified the update so that I'm not even using them.
I'm at a loss
CodePudding user response:
Answer from the comments:
You pass an instance WeeklyRoutinePlan
(routinePlan) to the method. I guess that this is detached, since is it loaded somewhere else. Could you try to pass the id directly as Long (routinePlan.getId()
). Alternatively try to merge the routinePlan
and assign the merged entity to routinePlan
again.
-> So the issue is that the entity i'd created from the DTO had an ID and so it was counting as a detached entity even though i wasnt trying to save that one.