Reputation: 155
I don't understand why my weight record Repository is not called in spite of my code. I have written some logs before and after the weight record repository call. In my console, I can see that logs are called before and after, but not my repository. I have a code 200 OK, but in my database, my data is always here. I don't understand why. My Tomcat port is set to port 7777. I can create and read data but not delete it.
Just below my code : ENTITY, CONTROLLER, REPOSITORY and SERVICE.
https://gitlab.com/genetquentin/openweighttracker_backend/-/tree/develop
/*ENTITY 'PERSON'*/
@Entity
@Table(name = "person")
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id_person")
private Long idPerson;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "id_initial_data")
private InitialData userInitData;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "id_user")
private AppUser appUserPerson;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "person")
private List<WeightRecord> weightsList = new ArrayList<WeightRecord>();
/* Getters and setters */
/*ENTITY 'WEIGHTRECORD'*/
@Entity
@Table(name = "weight_record")
public class WeightRecord implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id_weight_record")
private Long idWeightRecord;
@Column(name = "weight_record_date", nullable = true)
private LocalDate weightRecordDate;
//JsonIgnore annotation because circular problem with Jackson and entity.
@JsonIgnore
@ManyToOne
private Person person;
@Min(1)
@Max(635)
@Column(name = "weight_kg_record", nullable = true, precision = 1)
private Double weightKgRecord;
@Min(1)
@Max(99)
@Column(name = "percent_fat_mass", nullable = true, precision = 1)
private Double percentFatMass;
@Min(1)
@Max(99)
@Column(name = "percent_muscular_mass", nullable = true, precision = 1)
private Double percentMuscularMass;
@Min(1)
@Max(99)
@Column(name = "percent_body_water", nullable = true, precision = 1)
private Double percentBodyWater;
@Min(1)
@Max(99)
@Column(name = "percent_bone_mass", nullable = true, precision = 1)
private Double percentBoneMass
/* Getters and setters */
/*CONTROLLER*/
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/weights")
public class WeightRecordController {
@Autowired
WeightRecordServiceImpl weightRecordServiceImpl;
@Autowired
WeightRecordRepository weightRecordRepository;
@Autowired
AppUserRepository appUserRepository;
@Autowired
PersonRepository personRepository;
private final Logger logger = LoggerFactory.getLogger(WeightRecordController.class);
//TODO : Weight is not deleted and repository not called ?
// USE deleteinbatch method repository from jpa ?
@DeleteMapping("/{weightId}")
public ResponseEntity<WeightRecord> deleteWeigthById(@PathVariable("weightId") Long weightId, Principal principal) {
logger.info("DELETE /weights/{}", weightId);
try {
Long appUserConnectedId = this.getAppUserConnectedId(principal);
Person personConnected = personRepository.findById(appUserConnectedId).orElseThrow();
WeightRecord weightRecordToDelete = weightRecordRepository.findById(weightId).orElseThrow();
if(personConnected != null && personConnected.getWeightsList().contains(weightRecordToDelete)) {
logger.info("SERVICE FOR DELETING");
logger.info(weightRecordToDelete.getWeightKgRecord().toString());
return ResponseEntity.ok(weightRecordServiceImpl.deleteWeightById(weightId));
} else {
logger.info("BAD USER FOR BAD WEIGHT");
return ResponseEntity.notFound().build();
}
} catch (NoSuchElementException nse) {
return ResponseEntity.notFound().build();
}
}
}
/*REPOSITORY*/
@Repository
public interface WeightRecordRepository extends JpaRepository<WeightRecord, Long> {
}
/*SERVICE*/
@Service
public class WeightRecordServiceImpl implements WeightRecordService {
@Autowired
WeightRecordRepository weightRecordRepository;
@Autowired
AppUserRepository appUserRepository;
@Autowired
PersonRepository personRepository;
private final Logger logger = LoggerFactory.getLogger(WeightRecordServiceImpl.class);
public WeightRecord deleteWeightById(Long weightRecordToDeleteId) {
logger.info("THIS IS THE WEIGHT ID TO DELETE : {}", weightRecordToDeleteId);
WeightRecord weightRecordToDelete = weightRecordRepository.findById(weightRecordToDeleteId).orElseThrow();
weightRecordRepository.deleteById(weightRecordToDeleteId);
logger.info("Weight with id n°{} is deleted now", weightRecordToDelete.getIdWeightRecord());
return weightRecordToDelete;
}
Upvotes: 1
Views: 1138
Reputation: 19966
The reason
The reason of the issue is this part of the code
personConnected.getWeightsList().contains(weightRecordToDelete)
Why it happens?
There is a spring data property spring.jpa.open-in-view
and it is true
by default. It means that JPA Persistent Context (Hibernate Session) is opened during entire HTTP request. What is this spring.jpa.open-in-view=true property in Spring Boot?
If Persistent Context is opened, fetch = FetchType.LAZY
property in the code below doesn't work at all.
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "person")
private List<WeightRecord> weightsList = new ArrayList<WeightRecord>();
When you do personConnected.getWeightsList().conatins()
, Hibernate loads weightsList
in the background. That's why the last SQL log entry is logged.
select
weightslis0_.person_id_person as person_i8_3_0_
...
from
weight_record weightslis0_
where
weightslis0_.person_id_person=?
So when you delete a WeightRecord
, it remains in the Person.weightsList
, because of it was loaded by personConnected.getWeightsList().conatins()
.
When HTTP request is completed, Persistent Context becomes closed, and Hibernate flushes all changes to the database. There is cascade = CascadeType.ALL
on the Person
side, so Hibernate should keep in mind a deleted WeightRecord
in the weightsList
. So it does nothing, because you could delete WeightRecord
and insert it by some reasons again.
You can verify statements above just by removing personConnected.getWeightsList().conatins()
part of the code, delete will start work.
How to solve
Set spring.jpa.open-in-view=false
in the application.property
. So you will have LazyInitializationException
with personConnected.getWeightsList().conatins()
Remove personConnected.getWeightsList().conatins()
code. You can do the same just comparing WeightRecord.Person.id
and a current Person.id
.
Optional<Person> personConnected = personRepository.findById(appUserConnectedId);
if (personConnected.isPresent() ) {
WeightRecord weightRecordToDelete = weightRecordRepository.findById(weightId).orElseThrow();
Long userPersonId = personConnected.get().getIdPerson();
Long recordPersonId = weightRecordToDelete.getPerson().getIdPerson();
if (Objects.equals(userPersonId, recordPersonId)) {
logger.info("SERVICE FOR DELETING");
return ResponseEntity.ok(weightRecordServiceImpl.deleteWeightById(weightRecordToDelete));
}
}
logger.info("BAD USER FOR BAD WEIGHT");
return ResponseEntity.notFound().build();
Now you can keep cascade = CascadeType.ALL
on the weightsList
on Person
side.
Notes
Always use spring.jpa.open-in-view=false
. LazyInitializationException
is your friend.
Always use fetch = FetchType.LAZY
on the @ManyToOne
part of the association. It is FetchType.EAGER
by default.
@ManyToOne(fetch = FetchType.LAZY)
private Person person;
Never use Jackson annotations like @JsonIgnore
and Hibernate Validator annotations like @Min(1)
with entities.
Use PersonEntity
class for an entity and Person
for JSON. Do mapping on the service level.
Upvotes: 1
Reputation: 155
In fact, i have juste deleted the CascadeType.ALL from my @OneToMany entity Person and all is working. I can create, read and delete records from database.
But I'am going to investigate more this problem to understand in a better way its origin.
@Entity
@Table(name = "person")
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id_person")
private Long idPerson;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "id_initial_data")
private InitialData userInitData;
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "id_user")
private AppUser appUserPerson;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "person")
private List<WeightRecord> weightsList = new ArrayList<WeightRecord>();
Upvotes: 2