Hibernate order of Operations clashes with UniqueConstraint - spring-boot

The order in which Hibernate performs the delete/insert when updating a collection causes a unique constraint I want to define to fail. Hibernate tries to first insert new elements and then delete old ones. Inserting the new records causes my unique constraint to fail even though the database would be in a valid state after all operations have concluded.
My entities
#Entity
#Table(name = "Car")
public class Car {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "idCar")
private long idCar;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "car", orphanRemoval = true, fetch = FetchType.LAZY)
#Fetch(value = FetchMode.SUBSELECT)
private List<Wheel> wheels = new ArrayList<>();
// getters/setters ommitted
}
#Entity
#Table(name = "Wheel", uniqueConstraints = { #UniqueConstraint(columnNames = {"index", "idCar"})})
public class Wheel {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "idWheel")
private long idWheel;
#ManyToOne
#JoinColumn(name = "idCar")
private Car car;
#Column(name = "`index`")
private int index;
#Column(name = "name")
private String name;
// getters/setters ommitted
}
Example of Usage
public void createCar() {
Car car = new Car();
List<Wheel> wheels = new ArrayList<>();
wheels.add(new Wheel(car,1,"Continental"));
wheels.add(new Wheel(car,2,"Continental"));
wheels.add(new Wheel(car,3,"Continental"));
wheels.add(new Wheel(car,4,"Continental"));
car.setWheels(wheels);
carRepository.save(car);
}
public void updateCar(long idCar) {
Car car = carRepository.findById(idCar).get();
List<Wheel> wheels = new ArrayList<>();
wheels.add(new Wheel(car,1,"Pirelli"));
wheels.add(new Wheel(car,2,"Pirelli"));
wheels.add(new Wheel(car,3,"Pirelli"));
wheels.add(new Wheel(car,4,"Pirelli"));
car.setWheels(wheels);
carRepository.save(car);
}
This behavior seems to be intended and there is no way to modify hibernate to execute the deletes first.
This Bugreport was rejected
My database (MariaDB) sadly does not support deferred unique constraints which seem like the optimal solution. I could remove the constraint or modify my application code to first manually delete any orphans and flush but both of these solutions seem suboptimal.
Are there any better workarounds I have missed? What is the best practice approach?

When working with bidirectional association one has to keep the association in sync at all times (this is in the documentation).
You need to change your code to:
car.setWheels(wheels);
for (Wheel wheel : wheels) {
wheel.setCar(car);
}
Or even better, you can create a utility method:
car.addWheel(wheel);
class Car {
...
public void addWheel(Wheel wheel) {
this.wheels.add(wheel);
wheel.setCar(this);
}
...
}

Related

Automatic JPA refresh ManyToOne objects with #Version feature

I'm getting an exception:
org.hibernate.TransientPropertyValueException:
object references an unsaved transient instance
- save the transient instance before flushing :
com.example.jpamapstruct.entity.Member.club ->
com.example.jpamapstruct.entity.Club
while saving the member entity:
#Transactional
public MemberDto save(MemberDto memberDto){
Member entity = memberMapper.toEntity(memberDto);
return memberMapper.toDto(repository.save(entity));
}
How to fix this case in a proper way?
Possible solution:
I can get and set a club object before saving a member but is it only one and the best approach in such scenario?
Member entity = memberMapper.toEntity(memberDto);
clubRepository.getReferencedById(memberDto.getClubId()).ifPresent(entity::setClub);
return memberMapper.toDto(repository.save(entity));
Questions:
Should I put this getReferencedById code explicity? I mean what if we have several child objects (unidirectional ManyToOne), for each we need to get data from DB.
Is there any way to handle this by JPA (Spring Data/JPA) "automatically"?
Maybe it is possible to hit DB only one time with f.e join fetch somehow for all childs (with using custom #Query or querydsl or criteria/specification)?
Next, hoow to handle collections (unidirectional manyToMany)? In my case set of events in member object. Also need to loop thru and get all objects one by one before saving member?
Where should I put such logic in a service or maybe better in a mapstuct mapper?
If so, how to use repositories in such mapper?
#Mapper(componentModel = "spring")
public interface MemberMapper extends EntityMapper<MemberDto, Member> {
#AfterMapping
default void afterMemberMapping(#MappingTarget Member m, MemberDto dto) {
var club = clubRepo.findById(m.getClub().getId())
m.setClub(club)
}
Source code:
#Entity
public class Club extends AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
}
public class ClubDto extends AbstractDto {
private Long id;
}
#Entity
public class Member {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Long id;
// commented out as don't want to save child object as it should already exist
// #ManyToOne(cascade = CascadeType.ALL)
#ManyToOne
Club club;
#ManyToMany
#JoinTable(name = "member_events",
joinColumns = #JoinColumn(name = "member_id"),
inverseJoinColumns = #JoinColumn(name = "event_id")
)
List<Event> events = new ArrayList<>();
}
public class MemberDto {
private Long id;
private ClubDto club;
}
#MappedSuperclass
public abstract class AbstractEntity {
#Version
private Integer version;
}
public abstract class AbstractDto {
private Integer version;
}
//MemberMapper above

A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance - Spring and Lombok

I am getting this A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance error with my oneToMany relationship when trying to update my child element (report). Although I see this question asked a few times here, I haven't been able to make my code to work with them and I now feel it may be an issue with me using Lombok perhaps, since most of the answers here mention about changes on the hashcode and equals methods, which are abstracted away by Lombok? I tried to remove Lombok to try without it but then I got a bit confused on what to do next. If I could get some guidance on how to fix this issue within my original Lombok implementation please.
#Entity
#Table(name = "category")
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "category_title", nullable = false)
private String title;
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> report;
public Category(UUID id, String title) {
this.id = id;
this.title = title;
}
}
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "report")
#Data
public class Report {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "report_title", nullable = false)
private String reportTitle;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
#JoinColumn(name = "category_id", nullable = false)
private Category category;
public Report(UUID id) {
this.id = id;
}
}
#Override
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingReport.setCategory(category); // This is needed to remove hibernate interceptor to be set together with the other category properties
Report updatedReport = reportRepository.save(existingReport);
updatedReport.setCategory(category); // This is needed to remove hibernate interceptor to be set together with the other category properties
ReportUpdateDto newReportUpdateDto = new ReportUpdateDto(updatedReport.getId(),
updatedReport.getReportTitle(), updatedReport.getCategory());
return newReportUpdateDto;
} else {
return null;
}
}
Thank you very much.
Fast solution (but not recommended)
The error of collection [...] no longer referenced arrises in your code beacuse the synchronization between both sides of the bidiretional mapping category-report was just partially done.
It's important to note that binding the category to the report and vice-versa is not done by Hibernate. We must do this ouserselves, in the code, in order to sync both sides of the relationship, otherwise we may break the Domain Model relationship consistency.
In your code you have done half of the synchronization (binding the category to the report):
existingReport.setCategory(category);
What is missing is the binding of the report to the category:
category.addReport(existingReport);
where the Category.addReport() may be like that:
public void addReport(Report r){
if (this.report == null){
this.report = new ArrayList<>();
}
this.report.add(r);
}
Recommended Solution - Best practice for synchronizing both sides of the mapping
The suggested code above works, but it is error prone as the programmer may forget to call one of the lines when updating the relationship.
A better approach is to encapsulate that sychronization logic in a method in the owning side of the relationship. And that side is the Category as stated here: mappedBy = "category".
So what we do is to encapsulate in the Category.addReport(...) all the logic of cross-reference between Category and Report.
Considering the above version of addReport() method, what is missing is adding r.setCategory(this).
public class Category {
public void addReport(Report r){
if (this.reports == null){
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
}
Now, in the updateReport() it is enough to call the addReport() and the commented line bellow can be deleted:
//existingReport.setCategory(category); //That line can be removed
category.addReport(existingReport);
It is a good practice including in Category a removeReport() method as well:
public void removeReport(Report r){
if (this.reports != null){
r.setCategory = null;
this.reports.remove(r);
}
}
That is the code of Category.java after the two methods were added:
public class Category {
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
//Code ommited for brevity
public void addReport(Report r){
if (this.reports == null){
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
public void removeReport(Report r){
if (this.reports != null){
r.setCategory = null;
this.reports.remove(r);
}
}
}
And the code for updating a report category now is this:
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
existingCategory.addReport(existingReport);
reportRepository.save(existingReport);
return new ReportUpdateDto(existingReport.getId(),
existingReport.getReportTitle(), existingReport.getCategory());
} else {
return null;
}
}
A good resource to see a practical example of synchronization in bidirectional associations: https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/
Lombok and Hibernate - not the best of the combinations
Though we can not blame Lombok for the error described in your question, many problems may arrise when using Lombok alongside with Hibernate:
Properties being loaded even if marked for lazy loading...
When generating hashcode(), equals() or toString() using Lombok, the getters of fields marked as lazy are very likelly to be called. So the programmer's initial intention of postponing some properties loading will no be respected as they will be retrieved from the database when one of hascode(), equals() or toString() is invoked.
In the best case scenario, if a session is open, this will cause additional queries and slow down your application.
In the worst case scenarios, when no session is available, a LazyInitializationException will be thrown.
Lombok's hashcode()/equals() affecting the bevahior of collections
Hibernate uses hascode() and equals() logic to check if a object is order to avoid inserting the same object twice. The same applies to removing from a list.
The way Lombok generates the methods hashcode() and equals() may affect hibernate and create inconsistent properties (especially Collections).
See this article for more info on this subject: https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/
Lombok/Hibernate integration in a nutshell
Don't use Lombok for entity classes. Lombok annotations you need to avoid are #Data, #ToString, and #EqualsAndHashCode.
Off-topic - Beware of delete-orphan
In Category, the #OneToMany mapping is defined with orphanRemoval=true as bellow:
#OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
The orphanRemoval=true means that when deleting a category, all the reports in that category will be deleted as well.
It is important to assess if that is the desired behavior in your application.
See an example of the SQLs hibernate will execute when calling categoryRepository.delete(category):
//Retrieving all the reports associated to the category
select
report0_.category_id as category3_1_0_,
report0_.id as id1_1_0_,
report0_.id as id1_1_1_,
report0_.category_id as category3_1_1_,
report0_.report_title as report_t2_1_1_
from
report report0_
where
report0_.category_id=?
//Deleting all the report associated to the category (retrieved in previous select)
delete from
report
where
id=?
//Deleting the category
delete from
category
where
id=?
Just an update based on the accepted answer to avoid a StackOverflow and circular loop that came up after the changes.
I had to create a new Category object to remove the reports inside it within my return dto, otherwise as the category contains that same report, that again contains that category and so on, the infinite loop could be seen on my response.
#Override
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) {
if (reportRepository.findById(id).isPresent()) {
Report existingReport = reportRepository.findById(id).get();
existingReport.setReportTitle(reportUpdateDto.getTitle());
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingCategory.addReport(existingReport);
reportRepository.save(existingReport);
return new ReportUpdateDto(existingReport.getId(),
existingReport.getReportTitle(), existingReport.getRun_date(),
existingReport.getCreated_date(), category);
} else {
return null;
}
}
So added this part:
Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingCategory.addReport(existingReport);
As if I have something like
Category category = new Category(existingCategory.getId(), existingCategory.getTitle(), existingCategory.getReports);
I can see the issue once again, which is what the existingCategory object itself contains.
And here my final entities
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "report")
#Data
public class Report {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "report_title", nullable = false)
private String reportTitle;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
#JoinColumn(name = "category_id", nullable = false)
private Category category;
#Entity
#Table(name = "category")
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#Column(name = "category_title", nullable = false)
private String title;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
public Category(UUID id, String title) {
this.id = id;
this.title = title;
}
public void addReport(Report r) {
if (this.reports == null) {
this.reports = new ArrayList<>();
}
r.setCategory(this);
this.reports.add(r);
}
public void removeReport(Report r) {
if (this.reports != null) {
r.setCategory(null);
this.reports.remove(r);
}
}
}

Find the Values of multiple layer childs (using loop???)

I was not sure about how to search for this so was really unsuccessful. I will just start from data classes that i have,
Here is the TrainingPlan Entity (example)
#Data
#Entity
#Table(name = "TRAINING_PLAN")
#JsonIgnoreProperties({"trainings"})
public class TrainingPlan {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#Column(name = "CURRENT_PHASE")
private int currentPhase;
#Column(name = "PREVIOUS_TRAINING_PLAN_ID")
private Long previousTrainingPlanId;
#OneToMany(mappedBy = "trainingPlan", fetch = FetchType.LAZY)
private List<Training> trainings = new ArrayList<Training>();
Here is the Training Entity (example)
#Data
#Entity
#Table(name = "TRAINING")
public class Training {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "TRAINING_PLAN_ID")
private TrainingPlan trainingPlan;
I hope i will be able to explain this clearly, so what i am trying to achieve here is to get Trainings from all previous (TrainingPlan) phases.
How it works - when next (New TrainingPlan) plan phase is created it references the old Plan with the previousPlanId so it recognises its parent (its like layers).
So, for example, if TrainingPlan has phase 2, i can easily get Trainings from phase 1 in a similar way to this:
public List<Training> trainingsFromPreviousPhase(TrainingPlan plan, List<TrainingPlan> planList) {
List<Training> previousPhaseTrainings = new ArrayList<Training>();
if (plan.getPreviousTrainingPlanId() != null) {
TrainingPlan previousPlanPhase = planList.parallelStream()
.filter(e -> e.getId() == plan.getPreviousTrainingPlanId()).findAny().get();
previousPhaseTrainings = previousPlanPhase.getTrainings();
}
return previousPhaseTrainings;
}
Now the problem is if a plan is at Phase 5, i need to get the trainings from all 4 previous phases, and this is where i get stuck, cant think of a method to automatically loop that. I hope i explained this not too hard to understand. Any advices or questions are welcome.
What i need here is something similar to:
List<List<Training>> getTrainingsFromAllPreviousPhases()
You could loop through all plan phases like this:
TrainingPlan currentPlan = plan;
List<Training> allTrainings = new ArrayList<>();
while (cirrentPlan.getPreviousTrainingPlanId() != null) {
TrainingPlan previousPlanPhase = planList.parallelStream()
.filter(e -> e.getId() == currentPlan.getPreviousTrainingPlanId()).findAny().get();
allTrainings.addAll(previousPlanPhase.getTrainings());
currentPlan = previousPlanPhase;
}
However, to avoid repeatedly going through the list of plans to find the one by id, I'd propose to keep them in a map:
Map<Long, TrainingPlan> planMap = planList.stream()
.collect(Collectors.toMap(TrainingPlan::getId, Function.identity());
Then the code could be simplified (both visually, and in terms of O(n) complexity):
TrainingPlan currentPlan = plan;
List<Training> allTrainings = new ArrayList<>();
while (currentPlan.getPreviousTrainingPlanId() != null) {
TrainingPlan previousPlanPhase = planMap.get(currentPlan.getPreviousTrainingPlanId());
allTrainings.addAll(previousPlanPhase.getTrainings());
currentPlan = previousPlanPhase;
}

Spring-Data-Jpa bidirectional association with EmbeddedId. Foreign key is null when merging

I am running the latest version of Hibernate with 2 entities: Project and ProjectShare, which have a one-to-many relation set up bidirectional.
A ProjectShare is uniquely identified by a composite ID containing project_id and user_id. Besides the key, a ProjectShare contains a boolean flag whether the user gets read or write access on the project.
#Entity
#Table(name = "projects")
public class Project {
#Id
#GeneratedValue // UUID generator
#Column(name = "project_id")
private String id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProjectShare> shares = new ArrayList<>();
public Project(String name) {
this.name = name;
}
public void addShare(ProjectShare share) {
shares.add(share);
share.setProject(this);
}
public void removeShare(ProjectShare share) {
shares.remove(share);
share.setProject(null);
}
}
#Entity
#Table(name = "project_shares")
public class ProjectShare {
#EmbeddedId
private ProjectShareId id;
#Column(name = "has_write_access")
private boolean hasWriteAccess;
#MapsId("projectId")
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "project_id", nullable = false, updatable = false)
private Project project;
public ProjectShare(ProjectShareId id, boolean hasWriteAccess) {
this.id = id;
this.hasWriteAccess = hasWriteAccess;
}
public void setProject(Project project) {
this.project = project;
}
}
#Embeddable
public class ProjectShareId implements Serializable {
#Column(name = "project_id")
private String projectId;
#Column(name = "user_id")
private String userId;
public ProjectShareId(String userId) {
this.userId = userId;
}
// equals and hashCode go here...
}
If I create a new Project and assign new ProjectShare associations to it, everything works fine:
Project project = new Project("my_project");
project.addShare(new ProjectShare(new ProjectShareId("user1"), false));
project.addShare(new ProjectShare(new ProjectShareId("user2"), false));
projectRepository.save(project); // assume project id is '123'
Since all objects are new and not yet persisted, it executes 1 insert for the Project itself and 1 insert for each ProjectShare. Lets assume the project is inserted with id '123'.
Now, if I load this existing project, and add new ProjectShares to it, things go wrong:
Project project = projectRepository.findById("123");
project.addShare(new ProjectShare(new ProjectShareId("user2"), true));
project.addShare(new ProjectShare(new ProjectShareId("user3"), true));
projectRepository.save(project);
For every ProjectShare, this executes a SELECT on the values in ProjectShareId (project_id and user_id), followed by either an INSERT of UPDATE, depending on whether the record was found. This is the basic merge strategy of Hibernate and this is what we want.
The desired outcome of this should be:
Leave ProjectShare for user1 untouched
Update the ProjectShare for user2 (from false to true)
Create a new ProjectShare for user3
However, when the SELECT is executed for the ProjectShare, the foreign key project_id is always null. This means that existing records are never found, an INSERT is attempted instead of and UPDATE, and a DB-level Constraint violation is triggered.
How should I solve this issue? Should I manually go through the project.getShares() collection, find existing records and update them instead? I was hoping Hibernate would do this through its merge strategy.
Or could this be a bug in Hibernate related to associations with foreign keys in Embbedded IDs?

HIbernate + JPA OneToMany Lazy loading not working if no foreign key specified in the db

Hibernate lazy loading is not working in my code. It loads the entire data even it is specified as FetchType LAZY
#Transactional(propagation = Propagation.NEVER)
public OrderItem getItem(String itemId) throws Exception {
OrderItem item = itemDao.find(OrderItem.class, Integer.parseInt(itemId));
if (item == null) {
throw new Exception(502, "We are unable to load item for #" + itemId);
}
return item;
}
#NotFound(action = NotFoundAction.IGNORE)
#OneToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
#JoinColumn(name = "id_order_detail")
#Fetch(value= FetchMode.JOIN)
#JsonManagedReference
private Set<OrderItemStateChangeEntry> itemStateHistory;
I could not able to lazy load the contents. There is no foreign key constraint set in the db. And its not possible to set as the many parent data not present in the system.
Can somebody help me on this
Update
Added my class and reference. But lazy load work
#Entity
#Table(name = "ps_orders")
#AttributeOverrides({
#AttributeOverride(name="id",column=#Column(name="id_order")),
#AttributeOverride(name="createTime",column=#Column(name="date_add")),
#AttributeOverride(name="updateTime",column=#Column(name="date_upd"))
})
public class Order extends BaseEntity{
#Column(name = "id_carrier")
private Integer carrier = 0;
#NotFound(action = NotFoundAction.IGNORE)
#OneToMany(fetch = FetchType.LAZY,cascade={CascadeType.PERSIST, CascadeType.MERGE}, mappedBy="order")
#Fetch(FetchMode.SELECT)
#JsonManagedReference
private Set<OrderStateChangeEntry> orderHistory;
//Getters and Setters
}
#Entity
#Table(name = "ps_order_history")
#EnableBouncerProfile
public class OrderStateChangeEntry implements java.io.Serializable{
public OrderStateChangeEntry(){}
public OrderStateChangeEntry(Order order){
this.order = order;
}
#Id
#Column(name = "id_order_history")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name="id_order", nullable=false)
#JsonBackReference
private Order order;
//Getters and Setters
}
It is because of your
#Fetch(value= FetchMode.JOIN)
it disables the lazy loading ...
As you specify the fetch mode in your #OnetoMany relationship, i would say that you can simply remove that line above.

Resources