Spring data jpa #OneToMany bidirectional on same table - spring

I'm trying to create a simple mapping for a single table relation like an orgChart.
I'm using Spring Boot with Spring data and JPA.
This is my Entity:
#Entity
#Table(name = "orgchart")
public class OrgChart {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#ManyToOne
private OrgChart parent;
#OneToMany
#JoinColumn(name = "parent_id")
private List<OrgChart> children = new ArrayList<>();
}
and the test code to add some data:
OrgChart orgChart = new OrgChart();
OrgChart orgChart1 = new OrgChart();
orgChartRepository.save(orgChart1);
OrgChart orgChart2 = new OrgChart();
orgChartRepository.save(orgChart2);
orgChart.getChildren().add(orgChart1);
orgChart.getChildren().add(orgChart2);
OrgChart saved = orgChartRepository.save(orgChart);
so the table is created properly and the structure looks also ok to me:
id |parent_id|
---+---------+
361| |
359| 361|
360| 361|
but the problem is that I don't see the parent field in the object when I fetch it from the DB:
List<OrgChart> children = orgChartRepository.getById(saved.getId()).getChildren()
here children.get(0).getParent() is always null. What am I doing wrong? I've already tried so many ways, is this thing possible to achieve?
Thanks.

Try #OneToMany(mappedBy="parent") to tell JPA that these fields are the two ends of a bidirectional relationship, not two different unidirectional ones.

The following should accomplish it:
#OneToMany(cascade = CascadeType.ALL, mappedBy="parent", orphanRemoval = true)
private List<OrgChart> children = new ArrayList<>();
#ManyToOne
#JoinColumn(name = "parent_id")
private OrgChart parent;
OrgChart parent = new OrgChart();
OrgChart newOrgChart = new OrgChart();
newOrgChart.setParent(parent);
parent.getChildren().add(newOrgChart);

Related

Spring JPA: How to remove OneToOne related entities when master entity is removed?

First of all, thank you for interest in this question.
Here is the issue I am having with my relations.
I have a master entity which is #OneToOne referenced to 2 different tables. But the master entity has no references from those two tables.
#SQLDelete(sql = "UPDATE contract_reservation SET deleted = true WHERE id=?")
#Where(clause = "deleted=false")
public class ContractReservation extends AbsLongEntity {
#Column(nullable = false)
private String reservationNumber;
#Column(name = "date", nullable = false)
private LocalDate date;
#ManyToOne
private Company ownCompany;
Above is my master entity which is basis for two other entities. The code above is not complete, just a gist of what I have.
Below are the two other entities which has ContractReservation as their basis and #NotNull contract_reservation_id.
OriginalContract
#SQLDelete(sql = "UPDATE original_contract SET deleted = true WHERE id=?")
#Where(clause = "deleted=false")
public class OriginalContract extends AbsLongEntity {
#Column(nullable = false, unique = true)
private String contractNumber;
#OneToOne
#JoinColumn(nullable = false, unique = true)
private ContractReservation reservation;
private Boolean deleted;
FleetDashboard
#SQLDelete(sql = "UPDATE fleet_dashboard SET deleted = true WHERE id=?")
#Where(clause = "deleted=false")
public class FleetDashboard extends AbsLongEntity {
#OnDelete(action = OnDeleteAction.CASCADE)
#OneToOne(cascade = {
CascadeType.REMOVE
})
#JoinColumn(nullable = false)
private ContractReservation contractReservation;
#OnDelete(action = OnDeleteAction.CASCADE)
#OneToOne(cascade = {
CascadeType.REMOVE
})
private OriginalContract originalContract;
So far I have tried CascadeType.REMOVE, ALL. #OnDelete as seen in FleetDashboard entity but none of them worked so far. I also tried to write query but it is taking too long to respond.
What I want here is, when master entity(ContractReservation) is deleted, all related instances should also be deleted from related entities (OriginalContract, FleetDashboard).
For example, if I delete ContractReservation with id = 1, OriginalContract that has contract_reservation_id = 1 should also be deleted and so on. Main problem here seems #OneToOne relation and the fact that I have not referenced related entities in my master entity(ContractReservation).
Is there any way that can solve this issue without referencing related tables into master entity?
Thank you for your answers in advance.

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

Why Value is not getting assigned in JPA for insert statement

Hi I have couple of Entity classes as below, using lombok for getter and setters
Parent Entity Class have
#Table(name = "PARTY")
#Entity
public class Party {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "PARTY_ID")
#MapKey(name="dvpParticipantName")
#LazyCollection(LazyCollectionOption.FALSE)
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
}
Child Entity Class have
#Table(name = "DV_LOAN_PARTICIPANTS")
#Entity
public class DVLoanParticipants implements Serializable {
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
}
In service class i am calling save operation as
repository.save(parentEntityObject);
I am able to execute update statements ,but when i try to insert new row for child entity class i am getting an error saying
cannot insert NULL into ("ABC"."DV_LOAN_PARTICIPANTS"."PARTY_ID")
But if i print the parentEntityObject just before the save operation i see the values like
(partyId=12345678, dvpParticipantName=XYZ, dvpParticipantType=VKP)
I see the query formed as
insert
into
DV_LOAN_PARTICIPANTS
(DVP_PARTICIPANT_TYPE, PARTY_ID, DVP_PARTICIPANT_NAME)
values
(?, ?, ?)
Just before te save i am seeing valules in the Object
Builder=DVLoanParticipants(partyId=123456, dvpParticipantName=Builder,
dvpParticipantType=Individual)
Update
This is the setting part for values
DVLoanParticipants dvLoanParticipants = new
DVLoanParticipants();
dvLoanParticipants.setPartyId(Long.valueOf(partyId));
dvLoanParticipants.setDvpParticipantName("Builder");
dvLoanParticipants.setDvpParticipantType("Individual");
Party party = new Party();
Map<String, DVLoanParticipants> dvLoanParticipantsMap = new
java.util.HashMap<>();
dvLoanParticipantsMap.put("Builder", dvLoanParticipants);
party.setPartyId(Long.valueOf(partyId));
party.setDvLoanParticipantsMap(dvLoanParticipantsMap);
repository.save(party);
What is the mistake i am doing ?
The root cause of your problem in this part:
#OneToMany(targetEntity = DVLoanParticipants.class,cascade = CascadeType.ALL)
#JoinColumn(name = "LOAN_ID")
#MapKey(name="dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
actually for your case the column name in the #JoinColumn means:
If the join is for a unidirectional OneToMany mapping using a foreign key mapping strategy, the foreign key is in the table of the target entity.
So, assuming for the clarity that you want to map the following schema:
create table PARTY
(
PARTY_ID int,
-- ...
primary key (PARTY_ID)
);
create table DV_LOAN_PARTICIPANTS
(
PARTY_ID int,
DVP_PARTICIPANT_NAME varchar(50),
DVP_PARTICIPANT_TYPE varchar(10),
-- ...
primary key (PARTY_ID, DVP_PARTICIPANT_NAME),
foreign key (PARTY_ID) references PARTY(PARTY_ID)
);
You can use the following mapping:
#Entity
#Table(name = "PARTY")
public class Party
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
// I use fetch = FetchType.EAGER instead of deprecated #LazyCollection(LazyCollectionOption.FALSE)
// targetEntity = DVLoanParticipants.class is redundant here
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name = "PARTY_ID") // this is DV_LOAN_PARTICIPANTS.PARTY_ID column
#MapKey(name = "dvpParticipantName")
private Map<String, DVLoanParticipants> dvLoanParticipantsMap;
public Party()
{
dvLoanParticipantsMap = new HashMap<>();
}
// getters / setters
public void addParticipant(DVLoanParticipants p)
{
this.dvLoanParticipantsMap.put(p.getDvpParticipantName(), p);
p.setPartyId(getPartyId());
}
}
#Entity
#Table(name = "DV_LOAN_PARTICIPANTS")
public class DVLoanParticipants implements Serializable
{
#Id
#Column(name = "PARTY_ID")
private Long partyId;
#Id
#Column(name = "DVP_PARTICIPANT_NAME")
private String dvpParticipantName;
#Column(name = "DVP_PARTICIPANT_TYPE")
private String dvpParticipantType;
// getters / setters
}
and example how to save:
Party party = new Party();
party.setPartyId(2L);
// ...
DVLoanParticipants part1 = new DVLoanParticipants();
part1.setDvpParticipantName("Name 3");
part1.setDvpParticipantType("T1");
DVLoanParticipants part2 = new DVLoanParticipants();
part2.setDvpParticipantName("Name 4");
part2.setDvpParticipantType("T1");
party.addParticipant(part1);
party.addParticipant(part2);
repository.save(party);
and several notes:
The LazyCollectionOption.TRUE and LazyCollectionOption.FALSE values are deprecated since you should be using the JPA FetchType attribute of the #OneToMany association.
You use hibernate specific approach for mapping сomposite identifiers. As it's mentioned in the hibernate documentation:
The restriction that a composite identifier has to be represented by a primary key class (e.g. #EmbeddedId or #IdClass) is only JPA-specific.
Hibernate does allow composite identifiers to be defined without a primary key class via multiple #Id attributes.
But if you want to achieve more portability you should prefer one of the jpa allowed approaches.

How to update an entity using #onetomany relationship in JPA

I created an entity using a tree structure and parent child relations using #ManyToOne and #OneToMany annotations. However only changes I made to a parent of an entity are processed in the database.
My entity class looks like this:
#Entity
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class OKR {
#Id
#Column(name = "ID")
#GeneratedValue
private UUID id;
#NotBlank
private String name;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "parentid", referencedColumnName = "id")
private OKR parent;
#OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<OKR> children;
private boolean isRoot;
public OKR(String name, OKR parent, List<OKR> children){
this.name = name;
this.children = children;
if (this.children==null){
this.children = new ArrayList<>();
}
this.parent = parent;
if (parent == null||parent.equals(new UUID(0,0))){
isRoot = true;
}else{
isRoot = false;
}
}
protected OKR(){
isRoot = true;
children = new ArrayList<>();
}
When I update an OKR by changing its parent, the parent OKR is updated as well. However when I update an OKR by only adding a child, the child OKR does not get updated. I'm fairly new to JPA and I also noticed that there is no table for children inside my database. So my question is what is the easiest way to update all relations when only updating the children of an entity?

how to add #onetoone mapping for self entity in hibernate

how to add one-to-one mapping for the self entity. Like in this example. I want to have parent-child relationship for the Person itself.
#Entity
#Table(name="PERSON")
public class Person {
#Id
#Column(name="personId")
private int id;
#OneToOne
#JoinColumn()
private Person parentPerson;
}
Here is example of bidirectional self mapping #OneToOne (I change column names to SQL notation):
#Entity
#Table(name="PERSON")
public class Person {
#Id
#Column(name="person_id")
private int id;
#OneToOne
#JoinColumn(name = "parent_person_id")
private Person parentPerson;
#OneToOne(mappedBy = "parentPerson")
private Person childPerson;
}
But, I don't understand why you want to use #OneToOne in this case.
I am using it like this:
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "PARENT_ID", nullable = true)
private Person parent;
In order to add parent from your service layer, you need to already have at least one Person in the database.
Lets presume you do. Then create a new person. For e.g.:
#Transactional
public void createPerson() {
Person parent = //get your parent
Person child = new Person();
if (parent != null) {
child.setParent(parent);
}
}
If this is what you mean..

Resources