Quentin Genet
Quentin Genet

Reputation: 155

My repository is not called in my service - Spring Data JPA

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;
                }
        

My request in Insomnia:
My request in Insomnia

Result of request in console:
Result of request in console

Upvotes: 1

Views: 1138

Answers (2)

v.ladynev
v.ladynev

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

  1. Set spring.jpa.open-in-view=false in the application.property. So you will have LazyInitializationException with personConnected.getWeightsList().conatins()

  2. 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

Quentin Genet
Quentin Genet

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

Related Questions