Foreign key is null after saving objects - java

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

Related

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:
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>();

How to update recursive entities in Spring Data JPA

Actually I want to update an entity that contain a collection of another entity like this:
#Data
public class School{
#Id
#Column(name = "ID", nullable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
...
#OneToMany(fetch = FetchType.LAZY,
mappedBy = "tournee",
cascade = CascadeType.ALL)
private List<Student> students = new ArrayList<>();
}
and
#Data
public class Student{
#Id
#Column(name = "ID", nullable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ID_SCHOOL")
private School school;
}
but it shows me stack overflow error!
what is stack overflow error? and how can I resolve it?
this is my update function
#PutMapping("/update/{code}")
ResponseEntity<SchoolResponse> updateSchool(#RequestBody SchoolRequest
request,#PathVariable String code){
ModelMapper modelMapper = new ModelMapper();
SchoolDto schoolDto = schoolService.getSchoolByCode(code);
schoolDto = modelMapper.map(request,SchoolDto.class);
service.update(schoolDto);
}
I think that there is someting missing in my entities,
it shows me that there is an infinity recursive loop between school and student entities
Update
this is my service:
#Override
public SchoolDto updateSchool(SchoolDto school Dto) {
ModelMapper modelMapper = new ModelMapper();
School school = modelMapper.map(school Dto,School.class);
School updatedSchool = schoolRepository.save(school );
return modelMapper.map(updatedSchool,SchoolDto.class);
}

Springboot Mapping and DTO

I'm new to Spring and I'm encountering a lot of doubts when making an insertion of the example below.
I currently have three tables whose models are those below:
#Entity
#Table(name = "namespace")
public class Namespace {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#NotNull
#Size(max = 100)
#Column(unique = true)
private String namespacename;
}
#Entity
#Table(name = "services")
public class Services {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#NotNull
#Size(max = 100)
#Column(unique = true)
private String servicename;
}
#Entity
#Table(name = "historiquedeploiement")
public class HistoriqueDeploiement {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "idnamespace", nullable = false)
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
#JsonIdentityReference(alwaysAsId=true)
#JsonProperty("idnamespace")
private Namespace namespace;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "idservices", nullable = false)
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
#JsonIdentityReference(alwaysAsId=true)
#JsonProperty("idservices")
private Services services;
#NotNull
#Size(max = 100)
#Column(unique = true)
private String tagvalue;
}
And this is my DTO :
public class HistoriqueDeploiementReadingDTO {
#NotNull
private Integer id;
#NotNull
private String namespacename;
#NotNull
private String servicename;
#NotNull
private String tagvalue;
}
So the problem is :
I receive an object of type HistoriqueDeploiementReadingDTO and i have to insert it in historiquedeploiement table.
The problem is that i receive "namespacename", "servicename" and i need to save the id of each one that i can find in the table namespace and service.
When i have the id of each one, i can save it in historiquedeploiement table.
I hope you understand that little problem and hope you can purpose me something :)
Thanks !
You should first validate what you receive(against db records of each table). More or less the following will give you a highlight, so you should do for the others too.
Don't forget that all should be on the same transaction.
== updated==
#Transactional(rollbackFor=Exception.class)
public boolean saveHistoriqueDeploiement(HistoriqueDeploiementReadingDTO dto) {
Services service = getServices(dto.getServicename());
// do the same for the others
HistoriqueDeploiement deploiment = new HistoriqueDeploiement();
deploiment.setService(service);
//same for the others
deploiementRepository.save(deploiment);
}
public Services getServices(String serviceName) {
Services service = serviceRepository.findByServicename(serviceName); //if it exists, use the existing
if(service == null) {
service = new Services();
service.setServicename(dto.getServicename());
service = serviceRepository.save(service);
}
return service;
}
You have 2 ways:
First of all
if relation is many to one your field is List of services and List of namespaces instead services and namespaces.
if you mean OneToOne
HistoriqueDeploiementReadingDTO historiqueDeploiementReadingDTO;
NameSpace nameSpace = new Namespace();
namespace.setNameSpaceName(historiqueDeploiementReadingDTO.getNameSpaceName());
Services service = new Service();
service.setServicename(historiqueDeploiementReadingDTO.getServiceName())
HistoriqueDeploiement historiqueDeploiement = new HistoriqueDeploiement;
historiqueDeploiement.setTagValue(historiqueDeploiementReadingDTO.getTagValue())
historiqueDeploiement.setService(service)
historiqueDeploiement.setNameSpaceName(nameSpace)
IHistoriqueDeploiementRepository.save(historiqueDeploiement);
2 -

Spring Jpa Delete Operation Doesn't Work After Fetching Specific List

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.

Persist Nested Entity Spring Rest Data

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.

Categories

Resources