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:
Result of request in console:
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.
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>();
Related
I am facing an issue with persisting Observation entity.
The Observation entity has a list of protocols and every protocol has 1 observer. The observer can also be created before or not, so I can have many protocols which have created observers and many protocols which have not created observers.
The desired behavior for me is if I create this observation then observers with id == null will be created and observers with id will be merged.
The issue I am facing is if I specify CascadeType.Merge I can create observation with only the observers which were created before and if I specify CascadeType.Persist/CascadeType.All, I can create observation with only the observers that were not created before.
ObservationEntity:
#Entity
#Getter
#Setter
#NoArgsConstructor
public class Observation {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "observation_id", referencedColumnName = "id", nullable = false)
#OrderBy("observer ASC")
#Fetch(FetchMode.SUBSELECT)
private List<Protocol> protocols;
}
ProtocolEntity:
#Entity
#Getter
#Setter
#NoArgsConstructor
public class Protocol {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.MERGE)
#JoinColumn(name = "observer_id", nullable = false)
private Observer observer;
}
ObserverEntity:
#Entity
#Getter
#Setter
#NoArgsConstructor
public class Observer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false)
private String firstName;
#Column(nullable = false)
private String lastName;
private String imoCode;
}
ObservationService:
#Service
public class ObservationService {
#Autowired private ObservationMapper observationMapper;
#Autowired private ObservationRepository observationRepository;
public ObservationDTO create(ObservationDTO observationDTO) {
Observation observationCreated = observationRepository.save(observationMapper.observationDTO2Observation(observationDTO));
return observationMapper.observation2ObservationDTO(observationCreated);
}
}
Error:
org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation : sk.adambarca.serverspringboot.model.entity.Interval.protocol -> sk.adambarca.serverspringboot.model.entity.Protocol
Try this:
private List<Protocol> protocols = new ArrayList<>();
I'm trying to calculate the amount of achievements in runtime and set it into the application object inside the page but the totalAchivements goes outside the object.
Basically the totalAchievements should be inside of each application.
Controller
Page<UserApplication> userApplications = userApplicationRepository.findByUserUsername(pageable, username);
for(UserApplication userApplication : userApplications.getContent()) {
long totalAchievements = achievementRepository.countByApplicationApplicationId(userApplication.getApplication().getApplicationId());
userApplication.setTotalAchievements(totalAchievements);
}
return new ResponseEntity<>(userApplications, HttpStatus.OK);
Model
#Entity
#Table(name = "USER_APPLICATION")
public class UserApplication {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
#JoinColumn(name = "userId", referencedColumnName="userId")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
#JsonIgnore
private User user;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
#JoinColumn(name = "applicationId", referencedColumnName="applicationId")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private Application application;
#Transient
private long totalAchievements;
}
If you want the totalAchievements to be inside the application object, then you have to add the member inside the Application entity.
#Entity
class Application{
#Transient
private long totalAchievements;
}
Then set it as
userApplication.getApplication().setTotalAchievements(totalAchievements);
I have a relationship between Citizen:
#Entity
#Table(name = "citizens")
public class Citizen {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Size(max = 10, min = 10, message = "CPR must be exactly 10 characters")
private String cpr;
#OneToMany(mappedBy = "citizen", cascade = CascadeType.ALL, orphanRemoval = true)
private List<WeeklyCare> weeklyCare;
}
and WeeklyCare:
#Entity
public class WeeklyCare {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "citizen_id")
private Citizen citizen;
}
I have a REST API that recieves a list of Citizen each with a list of WeeklyCare and saves them:
#Autowired
private CitizenRepository citizenRepository;
#CrossOrigin(origins = "http://localhost:4200")
#PostMapping(path = "/add") // Map ONLY GET Requests
#Secured({"ROLE_ADMIN", "ROLE_DATAMANAGER"})
public ResponseEntity addNewCitizens(
#RequestBody List<Citizen> citizens) {
citizenRepository.saveAll(citizens);
return new ResponseEntity(new ApiResponse(true, "Filen er blevet indlæst", "CITIZENS_SAVED"), HttpStatus.OK);
}
After this, when I look in the weekly_care table in the database, all rows have null on the citizen_id column. What am I missing?
This is a common scenario in Hibernate and results from not setting the inverse of the relationship:
Citizen c = new Citizen();
WeeklyCare w = new WeeklyCare();
c.getWeeklyCare().add(w);
//The missing link:
w.setCitizen(c);
citizenRepository.save(c);
I'm not sure how this is configured in your web-service request though...
These are my entities;
#Entity
public class IpAddress{
#Id
#Column(nullable = false, updatable = false)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ")
#Getter
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
private final Application application;
.....
}
#Entity
public class Application{
#Column(nullable = false, updatable = false)
private final String applicationId;
#OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true)
private List<IpAddress> ipAddresses = new ArrayList<>();
}
#Entity
public class MerchantApplication {
#Id
#Column(nullable = false, updatable = false)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ")
#Getter
private Long id;
#OneToOne
private final Merchant merchant;
#OneToOne(cascade = CascadeType.ALL)
private final Application application;
#ManyToOne(fetch = FetchType.LAZY)
private final User user;
}
this is the service method that I try to delete:
#Transactional
public void deleteIpAddressForMerchant(Long merchantId,Long ipAddressId) {
Merchant merchant = merchantRepository.getMerchant(merchantId);
MerchantApplication merchantApplication = merchant.getMerchantApplication();
if (Objects.isNull(merchantApplication))
throw new ApplicationNotFoundException();
if (merchantApplication.getApplication().getIpAddresses().size() == 1) {
throw new IpAddressCannotBeDeleted();
}
IpAddress ipAddress = ipAddressRepository.getByIdAndApplication(ipAddressId, merchantApplication.getApplication());
ipAddressRepository.delete(ipAddress);
}
it works fine when i remove this block in service method;
if (merchantApplication.getApplication().getIpAddresses().size() == 1) {
throw new IpAddressCannotBeDeleted();
}
But this way, it's not work.Can you help me , what is the problem ?
If you fetch an entity field which has orphanRemoval = true property in a #Transactional method, it will save this field with your changes on it when the method returns.
In your case, you fetch
merchantApplication.getApplication().getIpAddresses()
But you didn't any change on it, then the fetched list was saved as it is.
You can remove the element in the fetched list what you want to delete.
you programmatically throw an exception in the following code:
if (merchantApplication.getApplication().getIpAddresses().size() == 1)
{
throw new IpAddressCannotBeDeleted();
}
by default transactions are rolled back if a runtime exception happens inside the method that is not handled (try...catch). Does your IpAddressCannotBeDeleted class extend RuntimeException? If so, try adding the following attribute to your #Transactional annotation:
#Transactional(noRollbackFor={IpAddressCannotBeDeleted.class, ApplicationNotFoundException.class})
The `noRollbackFor´ attribute prevents Spring from rolling back the transaction if exceptions of those two types are being thrown in your method implementation.
I have a User Class
#Entity(name = "users")
#Table(name = "users")
public class User implements UserDetails {
static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "user_id", nullable = false)
private Long id;
#Column(name = "username", nullable = false, unique = true)
private String username;
#Column(name = "password", nullable = false)
private String password;
}
Tied to a simple Repository
public interface UserRepository extends PagingAndSortingRepository<User, Long> {
}
And I have an Instructor Class that has a nested User object
#Entity
#Table(name = "instructors")
public class Instructor {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "instructor_id", nullable = false, updatable = false)
private Long id;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "user_id")
private User user;
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "course_id")
private List<Course> courses;
}
It is saved with the following repository
public interface InstructorRepository extends PagingAndSortingRepository<Instructor, Long> {
}
The JSON I am posting
{
"user": {
"id": 1
}
}
When I try to do a POST to /instructors . User is coming in null. Is there something I am missing to get JPA to tie the two together? I have tried adding CascadeType.ALL onto the field and that only throws a detached persist exception.
Leave the CascadeType.ALL to Instructor like you already tried:
#ManyToOne(fetch = FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumn(name = "user_id")
private User user;
In addition add the following to User. Seems to work with me. It provides the mapping information and makes JPA treat User managed
#OneToMany(mappedBy="user")//, cascade=CascadeType.ALL)
private List<Instructor> instructors = new ArrayList<>();
I have commented out the cascadeType in the above but it might be useful if you want to persist User wit all of its Instructors.