I am trying to implement a tree referencing itself (same class) with CRUD operations using Java and Hibernate. My class is :
#Entity
#Table(name="Person")
public class Person implements Comparable<Person>{
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
private String name;
#ManyToOne(cascade = {CascadeType.ALL})
private Person father;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
private List<Person> children = new ArrayList<Person>();
}
Insertion works good, at each insertion I set person's father to person and add person to father's children. While deleting, if I delete the person, it complains that person id is referenced by father, if I delete father, it complains that father id is referenced by person. So, what is the correct procedure of deleting or updating? There are similar questions, but I can not find the exact explanation for this bidirectional referencing problem.
So, I have found a solution to the problem thanks to #Al1's mapped byannotation. Still, after that I could not retrieve objects due to LazyInitializationException , but was able to delete the Leafs in a tree.
I have fixed that issue by changing private List<Person> children= new ArrayList<Person>(); to private Collection<Person> children = new LinkedHashSet<Person>();
The class now looks like:
public class Person implements Serializable{
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
private String name;
#ManyToOne(cascade = {CascadeType.ALL})
private Person father;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL}, mappedBy= "father")
private Collection<Person> children = new LinkedHashSet<Person>();
}
In order to delete the tree node, I had to to load the children by Hibernate.initialize(this.getChildren()); and then recursively delete every node. My function for deletion:
public static String deletePerson(Person p){
Transaction trns = null;
Session session = HibernateUtil.buildSessionFactory().openSession();
try {
trns = session.beginTransaction();
Hibernate.initialize(p.getChildren());
if (p.hasChildren()){
Collection<Person> children = p.getChildren();
for (Person person : children) {
deletePerson(person);
}
String hql = "delete from Person where name = :name";
session.createQuery(hql).setString("name", p.getName()).executeUpdate();
session.getTransaction().commit();
return "success";
}
else {
String hql = "delete from Person where name = :name";
session.createQuery(hql).setString("name", p.getName()).executeUpdate();
session.getTransaction().commit();
return "success";
}
} catch (RuntimeException e) {
if (trns != null) {
trns.rollback();
}
e.printStackTrace();
} finally {
session.flush();
session.close();
}
return "failure";
}
Hope this helps somebody who works with Hibernate and trees:)
Related
I am trying to add a friendship relationship between two persons using Spring MVC.
The first call goes well but the second one throws a Unique index or primary key violation?, why am i getting it?
#Entity
#Table(name="PERSON")
public class Person {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
#ManyToMany(fetch = FetchType.EAGER)
#ElementCollection
private List<Person> friends;
#ManyToMany(mappedBy="friends", fetch = FetchType.EAGER)
#ElementCollection
private List<Person> friendOf;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List<Person> getFriends() {
return friends;
}
public void setFriends(List<Person> friends) {
this.friends = friends;
}
public List<Person> getFriendOf() {
return friendOf;
}
public void setFriendOf(List<Person> friendOf) {
this.friendOf = friendOf;
}
public void addFriend(Person person){
if(this.getFriends() == null){
this.friends = new ArrayList<>();
}
this.friends.add(person);
}
public void setFriendship(Long firstPersonId, Long scndPersonId){
Person firstPerson = personService.getPerson(firstPersonId);
firstPerson.addFriend(personService.getPerson(scndPersonId));
personService.savePerson(firstPerson);
Person scndPerson = personService.getPerson(scndPersonId);
scndPerson.addFriend(personService.getPerson(firstPersonId));
personService.savePerson(scndPerson);
}
Person pup1 = new Person();
Long pupId1 = controller.savePerson(pup1);
Person pup2 = new Person();
long pupId2 = controller.savePerson(pup2);
setFriendship(pupId1, pupId2);
Person pup3 = new Person();
long pupId3 = controller.savePerson(pup3)
controller.setFriendship(pupId3, pupId1); //This throws an exception
controller.setFriendship(pupId3, pupId2);
Why is the marked line is causing an exception? The first call to setFrienship between p1 and p2 succeddes, but the when i try to make a connection between p1 and p3 it fails with the following exception :
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Unique index or primary key violation: "PUBLIC.UK_6VEJRSUXRONFC95I7O5XJNRMU_INDEX_D ON PUBLIC.PERSON_FRIENDS(FRIENDS_ID) VALUES 2
Please have a look at this answer,i hope you will get it right. The more you needed to do was to specific the join columns
Hibernate recursive many-to-many association with the same entity
There could also be a possibility that you trying to persist the same entity twice,in your case pub1 . Trying using merge instead!
Your setFriendship method calls the your service to save the entity which you have already done once,this is the reason that it gives you unique key violation as the entity is already there.
Leave comments if you need more explanation!
You need to specify join and inverse join columns directly. Please note that they are swapped on collections.
#ManyToMany
#JoinTable(name="friends",
joinColumns=#JoinColumn(name="personId"),
inverseJoinColumns=#JoinColumn(name="friendId")
)
private List<User> friends;
#ManyToMany
#JoinTable(name="friends",
joinColumns=#JoinColumn(name="friendId"),
inverseJoinColumns=#JoinColumn(name="personId")
)
private List<User> friendOf;
I would like to extend the requirements mentioned in the earlier post to support deletes. We have two data model object - Organization & Department sharing a one-to-many relationship. With the below mapping I am able to read the list of departments from the organization object. I have not added the cascade ALL property to restrict adding a department when creating an organization.
How should I modify the #OneToMany annotation (and possibly #ManyToOne) to restrict inserts of department but cascade the delete operation such that all associated departments are deleted when deleting an organization object?
#Entity
#Table(name="ORGANIZATIONS")
public class Organization{
#Id
#GeneratedValue
Private long id;
#Column(unique=true)
Private String name;
#OneToMany(mappedBy = "organization", fetch = FetchType.EAGER)
private List<Department> departments;
}
#Entity
#Table(name="DEPARTMENTS")
Public class Department{
#Id
#GeneratedValue
Private long id;
#Column(unique=true)
Private String name;
#ManyToOne(fetch = FetchType.EAGER)
private Organization organization;
}
The code to delete the organization is just a line
organizationRepository.deleteById(orgId);
The test case to validate this is as below
#RunWith(SpringJUnit4ClassRunner.class)
#DataJpaTest
#Transactional
public class OrganizationRepositoryTests {
#Autowired
private OrganizationRepository organizationRepository;
#Autowired
private DepartmentRepository departmentRepository;
#Test
public void testDeleteOrganization() {
final organization organization = organizationRepository.findByName(organizationName).get(); //precondition
Department d1 = new Department();
d1.setName("d1");
d1.setorganization(organization);
Department d2 = new Department();
d2.setName("d2");
d2.setorganization(organization);
departmentRepository.save(d1);
departmentRepository.save(d2);
// assertEquals(2, organizationRepository.getOne(organization.getId()).getDepartments().size()); //this assert is failing. For some reason organizations does not have a list of departments
organizationRepository.deleteById(organization.getId());
assertFalse(organizationRepository.findByName(organizationName).isPresent());
assertEquals(0, departmentRepository.findAll().size()); //no departments should be found
}
}
See code comments on why it fails:
#RunWith(SpringJUnit4ClassRunner.class)
#DataJpaTest
#Transactional
public class OrganizationRepositoryTests {
#Autowired
private OrganizationRepository organizationRepository;
#Autowired
private DepartmentRepository departmentRepository;
#PersistenceContext
private Entitymanager em;
#Test
public void testDeleteOrganization() {
Organization organization =
organizationRepository.findByName(organizationName).get();
Department d1 = new Department();
d1.setName("d1");
d1.setOrganization(organization);
Department d2 = new Department();
d2.setName("d2");
d2.setOrganization(organization);
departmentRepository.save(d1);
departmentRepository.save(d2);
// this fails because there is no trip to the database as Organization
// (the one loaded in the first line)
// already exists in the current entityManager - and you have not
// updated its list of departments.
// uncommenting the following line will trigger a reload and prove
// this to be the case: however it is not a fix for the issue.
// em.clear();
assertEquals(2,
organizationRepository.getOne(
organization.getId()).getDepartments().size());
//similary this will execute without error with the em.clear()
//statement uncommented
//however without that Hibernate knows nothing about the cascacding
//delete as there are no departments
//associated with organisation as you have not added them to the list.
organizationRepository.deleteById(organization.getId());
assertFalse(organizationRepository.findByName(organizationName).isPresent());
assertEquals(0, departmentRepository.findAll().size());
}
}
The correct fix is to ensure that the in-memory model is always maintained correctly by encapsulating add/remove/set operations and preventing
direct access to collections.
e.g.
public class Department(){
public void setOrganisation(Organisation organisation){
this.organisation = organisation;
if(! organisation.getDepartments().contains(department)){
organisation.addDepartment(department);
}
}
}
public class Organisation(){
public List<Department> getDepartments(){
return Collections.unmodifiableList(departments);
}
public void addDepartment(Department departmenmt){
departments.add(department);
if(department.getOrganisation() != this){
department.setOrganisation(this);
}
}
}
Try this code,
#OneToMany( fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "organisation_id", referencedColumnName = "id")
private List<Department> departments;
#ManyToOne(fetch = FetchType.EAGER,ascade = CascadeType.REFRESH,mappedBy = "departments")
private Organization organization;
if any issue inform
You can try to add to limit the cascade to delete operations only from Organization to department:
#OneToMany(mappedBy = "organization", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Department> departments;
Please note that if you have dependents/foreign key constraints on the department entity, then you would need to cascade the delete operations to these dependent entities as well.
You can read this guide, it explains the cascade operations nicely:
https://vladmihalcea.com/a-beginners-guide-to-jpa-and-hibernate-cascade-types/
I have an EJB many-to-many (bi-directional) relation between classes (entity-classes) Person and Hobby. There are corresponding tables in the database, called PERSON and HOBBY, as well as a table PERSON_HOBBY for the many-to-many relationship.
As I will detail below, the problem is that whenever I try to persist a person with hobbies, I run into a Foreign Key constraint violation. This is because the entityManager tries to save new rows into PERSON_HOBBY that contain references to a person-entity with ID=0, which doesn’t exist in the PERSON table. I’ll come back to that later, but first I’ll show the relevant parts of the entity classes.
First, here is entity class Person:
#Entity
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(nullable = false)
private String name;
#Column(nullable = false)
private String email;
#ManyToMany(cascade = {CascadeType.MERGE}, fetch = FetchType.EAGER)
/* Note: I used to have CascadeType.PERSIST in the above line as well, but
it caused "Detached object passed to persist" exceptions whenever I tried to
persist a person with hobbies. So I suppose I was right in deleting
CascadeType.PERSIST...? */
#JoinTable(name = "PERSON_HOBBY",
joinColumns = #JoinColumn(name="personId", referencedColumnName="id"),
inverseJoinColumns = #JoinColumn(name="hobbyId", referencedColumnName="id"))
private List<Hobby> hobbies = new ArrayList<Hobby>();
public List<Hobby> getHobbies () {
return hobbies;
}
public void setHobbies (List<Hobby> hobbies) {
this.hobbies = hobbies;
for(Hobby h:hobbies) { // this is to maintain bi-directionality
h.addPerson(this);
}
}
// other getters and setters omitted here.
Then entity class Hobby:
#Entity
public class Hobby {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(nullable = false)
private String description;
#ManyToMany(mappedBy = "hobbies", fetch = FetchType.EAGER)
private List<Person> persons;
public Hobby() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
// getter and setter for Description omitted here.
public List<Person> getPersons () {
return persons;
}
public void setPersons (List<Person> persons) {
this.persons = persons;
}
public void addPerson (Person p) {
this.persons.add(p);
}
}
I also have a stateless session bean, that’s shown here as far as relevant to the issue:
#Stateless
#Default
public class PersonRepositoryImpl implements PersonRepository {
#PersistenceContext
private EntityManager entityManager;
#Override
public Person create(Person p) {
entityManager.persist(p);
entityManager.flush();
return p;
}
#Override
public Person createPersonWithHobbies(Person p, List<Hobby>hobbyLijst) {
p = create(p); // I've also tried simply: create(p);
System.out.println("The newly assigned ID for the persisted
person is: " + p.getId());
// That last line **always** prints the person-ID as being 0 !!!!
p.setHobbies(hobbyLijst);
entityManager.merge(p); // This should save/persist the person's hobby's!
entityManager.flush();
return p;
}
}
Now from my servlet, I've been trying in two different ways. First, I tried calling method create(p) on the above session bean. That is, after creating a new Person instance p, setting all its non-relational fields, AND calling setHobbies on it (with a non-zero list of Hobby objects taken from the database), I called:
personRepo.create(p);
But this resulted in the Foreign Key (FK) exception:
INSERT on table 'PERSON_HOBBY' caused a violation of foreign key
constraint 'FK_EQAEPVYK583YDWLXC63YB3CXK' for key (0). The statement
has been rolled back.”
The FK-constraint mentioned here is the one in PERSON_HOBBY referring to PERSON.
The second way I tried was to make the following call from the servlet:
personRepo.createPersonWithHobbies(p, hobbyLijst);
where, just like before, p is the new person object; and hobbyLijst is that person's list of hobbies. And this resulted in the exact same FK-exception as the earlier call to personRepo.create(p).
Importantly, the println statement within method createPersonWithHobbies, calling getId() on the newly persisted person-object, ALWAYS gives that object's ID as being 0. Which I suppose does explain the FK-exception, since there's no person entity/row in the PERSON table with an ID of 0, nor is there supposed to be one. But of course the getId() call should not output 0. Instead, it should output the newly generated ID of the newly persisted person entity. (And yes, it IS persisted correctly in the PERSON tabel, with a correctly generated ID>0. So the correct ID is there in the PERSON-table - it just seems to be invisible to the entityManager and/or the container.)
Thanks.
I have written a service method importCategories() which retrieves a list of categories from database and recursively fills in properties and parent categories. The problem I'm experiencing is that new categories are created twice, except when I annotate complete() with #Transactional. Can anyone explain to me why that is? I save the child before adding it to the parent, and afterwards save the parent which has CascadeType.ALL on the child collection.
Model:
#Entity
public class Category implements Identifiable<Integer> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private Integer key;
private String name;
#ManyToOne
private Category parent;
#OneToMany(mappedBy="parent", cascade = {CascadeType.ALL})
private List<Category> children = new ArrayList<Category>();
public void add(Category category) {
category.setParent(this);
children.add(category);
}
}
Service:
#Transactional
private void complete(Category category) {
// ... (getting category info such as "name" and "parent key" from web service)
category.setName(name);
category = categoryRepository.saveAndFlush(category);
if (category.getParent() == null) {
Category parentCategory = new Category();
parentCategory.setKey(parentKey);
List<Category> categories = categoryRepository.findByKey(parentKey);
if (categories.size() > 0) {
parentCategory = categories.get(0);
}
parentCategory.add(category);
parentCategory = categoryRepository.saveAndFlush(parentCategory);
if (parentCategory.getParent() == null) {
complete(parentCategory);
}
}
}
public void importCategories() {
List<Category> list = categoryRepository.findAll();
for (Category category : list) {
complete(category);
}
}
If you have a cascade ALL type then you dont need to save your child entity first, just the parent.
category.getchildren().add(children)
save(category)
On that moment category will save/update the entity and will do the same for children.
look another examples to understand how works the hibernate cascade: http://www.mkyong.com/hibernate/hibernate-cascade-example-save-update-delete-and-delete-orphan/
I am getting the infamous "deleted object would be re-saved by cascade error" with Hibernate and I need some help to resolve it.
The entity model is as follows: There is a Person, Team and TeamMemberships between them. A Person can belong to many Teams and a Team can have many Persons linking to it, via TeamMembership objects.
Team
#OneToMany(mappedBy="team", cascade=CascadeType.ALL, fetch=FetchType.EAGER, orphanRemoval=true)
private List <TeamMembership> teamMemberships;
Person
#OneToMany(mappedBy="person", cascade=CascadeType.ALL, orphanRemoval=true)
private List <TeamMembership> teamMemberships;
TeamMembership
#ManyToOne
#JoinColumn(name="teamId", nullable=false)
private Team team;
#ManyToOne
#JoinColumn(name="personId", nullable=false)
private Person person;
So basically the TeamMembership is the "child" of both Person and Team. Everything works correctly when I edit team memberships from the team side. However when I try to edit team memberships from the person side I get this error.
Is the some mapping annotation of mine that is incorrect or that I am missing out? Any other solutions would be much appreciated.
Edit:
I process TeamMemberships on the team side via the TeamController as follows:
First I get the person ids from the jsp. The I call the following method:
private List<TeamMembership> processMembershipList (Team team, Long[] personIds, String teamRole) {
List<TeamMembership> memberships = team.getTeamMemberships();
boolean isNewTeam = false;
if (team.getId() == null) {
isNewTeam = true;
}
for (Long personId: personIds) {
TeamMembership teamMembership = null;
Person person = personService.getPerson(personId);
if (!isNewTeam) {
teamMembership = teamMembershipService.getTeamMembership(team, person, teamRole);
}
if (teamMembership == null) {
teamMembership = new TeamMembership();
teamMembership.setPerson(person);
teamMembership.setTeam(team);
teamMembership.setTeamRole(teamRole);
}
memberships.add(teamMembership);
}
return memberships;
}
And finally:
teamService.saveTeam(team);
On the Person side I do the same thing via the Person Controller:
Get the team ids and call the following method to process the team memberships:
private List<TeamMembership> processTeamMembershipList (Person person, Long[] teamIds, String teamRole) {
List<TeamMembership> memberships = person.getTeamMemberships();
boolean isNewPerson = false;
if (person.getId() == null) {
isNewPerson = true;
}
for (Long teamId: teamIds) {
TeamMembership teamMembership = null;
Team team = teamService.getTeam(teamId);
if (!isNewPerson) {
teamMembership = teamMembershipService.getTeamMembership(team, person, teamRole);
}
if (teamMembership == null) {
teamMembership = new TeamMembership();
teamMembership.setPerson(person);
teamMembership.setTeam(team);
teamMembership.setTeamRole(teamRole);
}
memberships.add(teamMembership);
}
return memberships;
}
And finally:
personService.savePerson(person);