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

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

Related

Creating subcategories in kotlin spring boot

I need to implement categories and subcategories within my entities. Here's what I have so far and think it should be:
StockCategory.kt
#Entity
#Table(name = "table_categories")
data class StockCategory(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "category_id")
val id: Long? = null,
#ManyToOne
#JoinColumn(name = "parentid")
val parent: StockCategory? = null,
#ManyToMany(mappedBy = "categories")
var stockItems: MutableList<StockItem> = mutableListOf(),
#OneToMany(mappedBy = "parent")
var childCategories: MutableList<StockCategory> = mutableListOf(),
)
StockItem.kt
#Entity
#Table(name = "table_stock")
data class StockItem(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "stock_item_id")
val id: Long? = null,
#Column(name = "stock_item_name")
var name: String = "New Item",
...
#ManyToMany
#JoinColumn(name = "item_category", referencedColumnName = "category_id")
var categories: MutableList<StockCategory> = mutableListOf(),
...
)
Now at the moment, this looks to be correct... At the very least Spring Boot is not complaining.
However, in terms of what to do next, I'm not sure. I know I need to implement a JpaRepository, of which I have the current:
StockCategoryRepository.kt
interface StockCategoryRepository: JpaRepository<StockCategory, Long> {
}
I also need to implement the relevant methods in my service class.
What exactly do I need to do next in order to get this to work and be able to use the information later on? Please also ELI5 too as although I have a decent amount of knowledge on this, I'm still not where I would like to be when it comes to this.
A few background bits if it makes it easier for you.
I'm using H2 as my database, Spring Boot and Kotlin as my language.

Spring Data JPA, change to one attribute of Many To Many entity is wrongly being shown on all other entities that share it

When I make changes to one attribute of an entity, it also somehow gets changed for every other entity that uses that entity. I have three entities as you can see below.
Students and courses need to have a many-to-many relationship between them and the course needs to have a one-to-many relationship with course lectures.
When I make changes to courses or course lectures that belong to a specific student by doing #Transactional student.getCourse().get(0).setTitle("whatever"), those changes are also reflected in other students who share the same course. I need help with this, thank you
The student class
public class Student {
#Id
#SequenceGenerator(
name = "student_sequence",
sequenceName = "student_sequence",
allocationSize=1
)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "student_sequence")
private Long studentId;
private String fullName;
#Column(name = "email_address", nullable = false)
private String email;
private String username;
private String password;
#ManyToMany(mappedBy = "students", fetch = FetchType.EAGER)
private List<Course> courses ;
public void addCourse(Course course) {
if (courses == null) {
courses = new ArrayList<>();
}
courses.add(course);
}
Course Class
public class Course {
#Id
#SequenceGenerator(
name = "course_sequence",
sequenceName = "course_sequence",
allocationSize = 1
)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "course_sequence")
private Long courseId;
private String title;
private double courseRating = 0;
private LocalDateTime createdAt = LocalDateTime.now();
private double completedProgress = 0;
#Embedded
private CourseInformation courseInformation;
#OneToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
#JoinColumn(name = "course_id", referencedColumnName = "courseId")
private List<CourseLecture> courseLectures;
#ManyToMany(
cascade = CascadeType.MERGE,
fetch = FetchType.LAZY
)
#JoinTable(
name = "student_course_mapping",
joinColumns = #JoinColumn(
name = "course_id",
referencedColumnName = "courseId"
),
inverseJoinColumns = #JoinColumn(
name = "student_id",
referencedColumnName = "studentId"
)
)
#ToString.Exclude
private List<Student> students;
There is no relationship mapping in the CourseLecture class.
This is not wrong, but just the way JPA works.
Technically it works, because they all reference the same instance as JPA guarantees to always return the same instance for a given class and id in single session.
If you don't want that you'd have to do the work either in different sessions, or you have to change your data model, so that each student has their own course. Of course this would be a strange model.
Update based on your comment:
Looks like indeed you need a different model, instead of Student -N-M-> Course you need something like Student -1-N-> Attendance -N-1-> Course, making the mapping table of your relationship into an entity and allowing it to store extra data that is specific to Student AND Course

Hibernate order of Operations clashes with UniqueConstraint

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

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

Criteria API Specification - Filter records and return only the latest record for many to one mappings

I have two tables, Lead and LeadActivity. A lead can have many lead activities and mapping is done as #ManyToOne form LeadActivity to Lead.
Problem Statement -I want to to filter LeadActivity records such that, If there are more than one leadactivity records with same leadId, i should get only one record which is latest (have max primary key). Can anyone guide me on how to write specification or criteria API in such situations? I know this can be achieved through other ways but I have to use specification API. Below are the entity classes
Lead
#Entity
#Table(name = "customer_lead")
#Where(clause = ReusableFields.SOFT_DELETED_CLAUSE)
#Audited(withModifiedFlag = true)
#Data
public class Lead extends ReusableFields implements Serializable
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "lead_id", updatable = false, nullable = false)
Long leadId;
#Column(name = "name")
String customerName;
#Column(name = "primary_mobile")
String primaryMobile;
#Column(name = "secondary_mobile")
String secondaryMobile;
//more fields
}
Lead Activity
#Entity
#Table(name = "LeadActivity")
#Data
#Where(clause = ReusableFields.SOFT_DELETED_CLAUSE)
public class LeadActivity extends ReusableFields implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "leadactivity_id", updatable = false, nullable = false)
Long leadActivityId;
#Column(name = "activity_date_time", nullable = false)
#NonNull
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm")
Date activityDateTime;
#Column(name = "title")
#NonNull
String title;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "lead_id", nullable = false)
#JsonIgnoreProperties(
{ "hibernateLazyInitializer", "handler" })
#NotFound(action = NotFoundAction.IGNORE)
Lead lead;
//More fields
}
Expected Output - Suppose there are two records present with same leadId. I want to fetch only the latest among them based on their id. One with lower id should be ignored
Try this:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<LeadActivity> cq = cb.createQuery(LeadActivity.class);
Root<LeadActivity> rootLeadActivity = cq.from(LeadActivity.class);
Join<LeadActivity,Lead> joinLead = rootLeadActivity.join(LeadActivity_.lead,JoinType.INNER);
/* If you dont use metamodel:
* Join<LeadActivity,Lead> joinLead = rootLeadActivity.join("lead",JoinType.INNER);
*/
// For performance, if you use JPA 2.1 set the leader id condition in the join
joinLead.on(cb.equal(joinLead.get(Lead_.leadId),LEAD_ID));
List<Predicate> predicatesList= new ArrayList<>();
/* if you use version 2.0 you will have to put it in the where
* predicatesList.add(cb.equal(joinLead.get(Lead_.leadId),LEAD_ID));
*/
Subquery<Long> sqMaxId = cq.subquery(Long.class);
Root<LeadActivity> sqRootActivity = sqMaxId.from(LeadActivity.class);
Join<LeadActivity,Lead> sqJoinLead = sqRootActivity.join(LeadActivity_.lead,JoinType.INNER);
sqMaxId.where(cb.equal(sqJoinLead.get(Lead_.leadId),joinLead.get(Lead_.leadId)));
sqMaxId.select(cb.max(sqRootActivity.get(LeadActivity_.leadActivityId)));
predicatesList.add(cb.equal(rootLeadActivity.get(LeadActivity_.leadActivityId),sqMaxId.getSelection()));
cq.where(predicatesList.toArray(new Predicate[predicatesList.size()]));
cq.multiselect(rootLeadActivity);
The result query:
select a.* from lead_activity a
inner join lead l on a.lead_id = l.lead_id and l.lead_id = LEAD_ID
where a.lead_activity_id =
(select max(lead_activity_id) from lead_activity where lead_id = LEAD_ID)

Resources