Hibernate many-to-many cascading delete - java

I have 3 tables in my database: Students, Courses and Students_Courses
Students can have multiple courses and courses can have multiple students. There is a many-to-many relationship between Students and Courses.
I have 3 cases for my project and courses added to my Courses table.
(a) When I add a user, it gets saved fine,
(b) When I add courses for the student, it creates new rows in User_Courses - again, expected behaviour.
(c) When I am trying to delete the student, it is deleting the appropriate records in Students and Students_Courses, but it is also deleting Courses records which is not required. Even if I don't have any user in a course, I want the course to be there.
Below is my code for tables and annotate classes.
CREATE TABLE `Students` (
`StudentID` INT(11) NOT NULL AUTO_INCREMENT,
`StudentName` VARCHAR(50) NOT NULL
PRIMARY KEY (`StudentID`)
)
CREATE TABLE `Courses` (
`CourseID` INT(11) NOT NULL AUTO_INCREMENT,
`CourseName` VARCHAR(50) NOT NULL
PRIMARY KEY (`CourseID`)
)
CREATE TABLE `Student_Courses` (
`StudentId` INT(10) NOT NULL DEFAULT '0',
`CourseID` INT(10) NOT NULL DEFAULT '0',
PRIMARY KEY (`StudentId`, `CourseID`),
INDEX `FK__courses` (`CourseID`),
INDEX `StudentId` (`StudentId`),
CONSTRAINT `FK__courses` FOREIGN KEY (`CourseID`) REFERENCES `courses` (`CourseID`) ON DELETE NO ACTION,
CONSTRAINT `FK_students` FOREIGN KEY (`StudentId`) REFERENCES `students` (`StudentId`)
)
This is the Java code generated by Hibernate:
#Entity
#Table(name = "Students")
public class Students implements java.io.Serializable {
private Integer StudentID;
private String Students;
private Set<Courses> Courseses = new HashSet<Courses>(0);
public Students() {
}
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "StudentID", unique = true, nullable = false)
public Integer getStudentID() {
return this.StudentID;
}
public void setStudentID(Integer StudentID) {
this.StudentID = StudentID;
}
#Column(name = "Students", nullable = false, length = 50)
public String getCampaign() {
return this.Students;
}
public void setCampaign(String Students) {
this.Students = Students;
}
#ManyToMany(cascade = {CascadeType.ALL}, fetch = FetchType.LAZY)
#JoinTable(name = "Student_Courses", joinColumns = {
#JoinColumn(name = "StudentId", nullable = false, updatable = false)}, inverseJoinColumns = {
#JoinColumn(name = "CourseID", nullable = false, updatable = false)})
public Set<Courses> getCourseses() {
return this.Courseses;
}
public void setCourseses(Set<Courses> Courseses) {
this.Courseses = Courseses;
}
}
#Entity
#Table(name = "Courses")
public class Courses implements java.io.Serializable {
private Integer CourseID;
private String CourseName;
private Set<Students> Studentses = new HashSet<Students>(0);
public Courses() {
}
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "CourseID", unique = true, nullable = false)
public Integer getCourseID() {
return this.CourseID;
}
public void setCourseID(Integer CourseID) {
this.CourseID = CourseID;
}
#Column(name = "CourseName", nullable = false, length = 100)
public String getCourseName() {
return this.CourseName;
}
public void setCourseName(String CourseName) {
this.CourseName = CourseName;
}
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "Courseses")
public Set<Students> getStudentses() {
return this.Studentses;
}
public void setStudentses(Set<Students> Studentses) {
this.Studentses = Studentses;
}
}
How can I achieve what I have described? I could not find any reasonable documentation on the web.

I found the correct mapping (and tested that with JUnit with an extensive case) in a similar scenario. I don't think I am going to post testing code because it would take long time to adapt to this example. Anyway the key is to:
Not use mappedBy attribute for the annotations, use join columns
List the possible CascadeTypes excluding REMOVE
In OP's example
#ManyToMany(fetch = FetchType.LAZY,
cascade =
{
CascadeType.DETACH,
CascadeType.MERGE,
CascadeType.REFRESH,
CascadeType.PERSIST
},
targetEntity = Course.class)
#JoinTable(name = "XTB_STUDENTS_COURSES",
inverseJoinColumns = #JoinColumn(name = "COURSE_ID",
nullable = false,
updatable = false),
joinColumns = #JoinColumn(name = "STUDENT_ID",
nullable = false,
updatable = false),
foreignKey = #ForeignKey(ConstraintMode.CONSTRAINT),
inverseForeignKey = #ForeignKey(ConstraintMode.CONSTRAINT))
private final Set<Course> courses = new HashSet<>();
#ManyToMany(fetch = FetchType.LAZY,
cascade =
{
CascadeType.DETACH,
CascadeType.MERGE,
CascadeType.REFRESH,
CascadeType.PERSIST
},
targetEntity = Student.class)
#JoinTable(name = "XTB_STUDENTS_COURSES",
joinColumns = #JoinColumn(name = "COURSE_ID",
nullable = false,
updatable = false),
inverseJoinColumns = #JoinColumn(name = "STUDENT_ID",
nullable = false,
updatable = false),
foreignKey = #ForeignKey(ConstraintMode.CONSTRAINT),
inverseForeignKey = #ForeignKey(ConstraintMode.CONSTRAINT))
private final Set<Student> students = new HashSet<>();
Extensive JUnit testing verified that:
I can add courses to students and vice versa flawlessly
If I remove a course from a student, the course is not deleted
Vice versa
If I remove a student, all courses are detached but they are still persisted (to other students) in database
Vice versa

Based on what you've told me you don't want cascade=CascadeType.ALL on the getCourseses method in Student. Keep in mind that Hibernate cascades are not the same as database cascades. Even if you don't have any cascades then Hibernate will delete the Students_Courses record.
The best way to think of Hibernate cascades is that if you call an operation on an entity and that operation is listed in the cascade list then that operation will be called on all of the child entities.
For example, when you call delete on Student, since delete is in the cascade list for Courses, Hibernate will call delete on each of the Course entities referenced by that student. That is why you are seeing the Course records disappearing.
Don't worry about database cascades, Hibernate will take care of those on its own.

You just need to Remove cascade = CascadeType.ALL in Student class only no change is required in Courses class
and add the below code
cascade = {CascadeType.PERSIST,CascadeType.MERGE,CascadeType.DETACH}..
It means while deleting owner class record it will not delete a non-owner record.
After this, On Delete it will delete only from Student table and student_course.
course table data remains the same.

Related

How to delete only the relation in a many to many relationship?

I have the following three tables:
player (id, name)
status (id, status_text)
player_status (player_id, status_id)
The player_status table combines the first two tables with a n-to-n relationship
Table "player":
id
player
agzua76t34gusad
"Anna"
sdahb433tbjsdbv
"Julia"
Table "status":
id
status_text
jjbsdnv8677v6df
"operational"
bulsiu783fdszjh
"violated"
Table "player_status"
record_id
record_status_id
agzua76t34gusad
jjbsdnv8677v6df
sdahb433tbjsdbv
bulsiu783fdszjh
The player can have a status assigned or not.
Now when a player has a status, how can I remove this status, so that the player but also the status stays in the tables but only the relation in the player_status table will be removed.
These are the classes for Player and Status
#Entity
#Table(name = "player")
public class Player {
#Column(nullable = false)
private String id;
#Column(nullable = false)
private String name;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
#JoinTable(name = "player_status",
joinColumns = {#JoinColumn(name = "player_id", referencedColumnName = "id")},
inverseJoinColumns = {#JoinColumn(name = "status_id", referencedColumnName = "id")})
private Set<Status> statusList = new HashSet<>();
}
#Entity
#Table(name = "status")
public class Status {
#Column(nullable = false)
private String id;
#Column(name = "status_text", nullable = false)
private String statusText;
#ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
}, mappedBy = "statusList")
#JsonIgnore
private Set<Player> players = new HashSet<>();
}
This is how the relation table is created in *.sql:
create table player_status
(
player_id varchar references player (id) on update cascade on delete cascade,
status_id varchar references status (id) on update cascade
);
How can I delete only an entry from the player_status table? I tried to retrieve a player from the db, changed/removed his status, but this did not update the player.
You just remove the associated Status from the statusList collection, and the row of the association table will be automatically deleted when the EntityManager is flushed.

JPA #JoinTable with composite (2 column) primary keys

In a spring-boot app, I've got the following entity definition:
#Data
#Entity
#Table(name = "users")
public class User {
#Id
#Column(nullable = false, name = "username", length = 100)
private String username;
#JoinTable(name = "userrole",
joinColumns = { #JoinColumn(name = "username") },
inverseJoinColumns = { #JoinColumn(name = "role") }
)
#OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Role> roles;`
I'm using Spring-data-jpa,Hibernate with H2 as the database.
The trouble is that spring-data-jpa, hibernate always generate/creates the join table (DDL) 'userrole' with a single column primary key. e.g. 'username'.
Hence, if records such as {'username', 'user_role'} and {'username', 'admin_role'} is inserted in the join table ('userrole'), the next insert fails with an error due to the 'duplicate' primary key.
I've tried using both columns in the above definition, as well as the following variation:
#OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
#JoinColumns({
#JoinColumn(name = "username"),
#JoinColumn(name = "role") })
private List<Role> roles;`
But that they resulted in the same or worse problems, e.g. and in the latter, even table creation fails because only a single column is used as primary key for the jointable. Role is simply another table with 2 columns 'role' and 'description', basically a role catalog.
How do we specify to JPA that the #JoinTable should use both 'username' and 'role' columns as composite primary keys?
edit:
I tried using an independent table/entity as suggested, thanks #Kamil Bęben
#Data
#Entity
#Table(name = "users")
public class User {
#Id
#Column(nullable = false, name = "username", length = 100)
private String username;
#OneToMany(
fetch = FetchType.EAGER,
cascade = CascadeType.ALL,
mappedBy = "username",
orphanRemoval = true
)
#ElementCollection
private List<UserRole> roles;
UserRole is defined as such
#Data
#NoArgsConstructor
#Entity
#Table(name = "userrole")
public class UserRole {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "userrole_seq")
Long id;
#Column(nullable = false, name = "username", length = 100)
private String username;
#Column(nullable = false, name = "role", length = 50)
private String role;
the repository for that user-roles join table is defined as
#Repository
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
UserRole findByUsernameAndRole(String username, String role);
List<UserRole> findByUsername(String username);
List<UserRole> findByRole(String role);
}
Admittedly, ugly, but that it works. And that somehow, it seemed to use the correct findByUsername() method to retrieve the roles as is relevant to the user, probably related to the 'mappedBy' clause. 'black magic'! There's lots more that I'd still need to find my way around JPA, Spring, Spring-data
edit2:
further update:
the original #JoinTable works as well.
But that the relations need to be specified as #ManyToMany
#ManyToMany(
fetch = FetchType.EAGER,
cascade = CascadeType.MERGE
)
#JoinTable(name = "usersroles",
joinColumns = { #JoinColumn(name = "username") },
inverseJoinColumns = { #JoinColumn(name = "role") }
)
private List<Role> roles = new ArrayList<Role>();
This creates 2 column primary keys as expected for the 'users-roles' table
Thanks to #Roman
If Role only has two columns, eg user_id and role, the way to map this in jpa would be as following
#ElementCollection
#CollectionTable(name = "user_roles", joinColumns = #JoinColumn(name = "user_id"))
#Column(name = "role")
List<String> roles = new ArrayList<>();
Otherwise, jpa really requires each entity's identifier and join columns to be separate columns, so Role entity would have to have columns like id, user_id and role_name. Could look like this .:
class Role {
#Id
Long id;
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id");
User user;
String roleName;
// Other fields
}
And in the User entity
#OneToMany(mappedBy = "user") // user is Field's name, not a column
List<Role> roles = new ArrayList<>();
Further reading

Bidirectional OneToMany and ManyToOne returns "NULL not allowed for column" on save

This is a shortened version of the entities where I only show the relevant parts.
#Entity
#Data
public class Wrapper {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id
#OneToOne(mappedBy = "wrapper", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private Application application;
public Wrapper(Application application) {
this.application = application;
application.setWrapper(this);
}
}
#Data
#Entity
#EqualsAndHashCode(exclude = "wrapper")
public class Application {
#Id
private Integer id;
#JsonIgnore
#OneToOne
#JoinColumn(name = "id")
#MapsId
private Wrapper wrapper;
#OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
#SortNatural
private SortedSet<Apartement> ownedApartements = new TreeSet<>();
}
#Entity
#Data
public class Apartement {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "application_id", insertable = false, updatable = false)
private Application application;
}
#Repository
public interface WrapperRepository extends JpaRepository<Wrapper, Integer> {
}
The above entities generates the following create table statements:
create table Wrapper (
id int identity not null,
primary key (id)
)
create table Application (
id int not null,
primary key (id)
)
create table Apartement (
id int identity not null,
application_id int not null,
primary key (id)
)
alter table Apartement
add constraint FKsrweh1i1p29mdjfp03or318od
foreign key (application_id)
references Application
alter table Application
add constraint FKgn7j3pircupa2rbqn8yte6kyc
foreign key (id)
references Wrapper
Given the follow entities and the following code:
Apartement apartement1 = new Apartement()
Apartement apartement2 = new Apartement()
Wrapper wrapper = new Wrapper(new Application());
Application application = wrapper.getApplication();
application.getOwnedApartements().addAll(Arrays.asList(apartement1, apartement2));
apartement1.setApplication(application);
apartement2.setApplication(application);
WrapperRepository.saveAndFlush(wrapper);
I see three inserts in the log.
First wrapper, then application, and finally apartement. But for some reason application_id is null on the first save. But I know it has a bi-directional relationship.
The error I get is:
Caused by: org.h2.jdbc.JdbcSQLException: NULL not allowed for column "APPLICATION_ID"; SQL statement:
insert into Apartement (id) values (null) [23502-197]
Why does this happen? Do I need to store everything in the correct order? Do I need to first store wrapper and application, then finally store the apartement once I have application ID?
Cannot hibernate store all three in one go? Or figure this out it self?
Sorry I fixed it.
The problem was
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "application_id", insertable = false, updatable = false)
private Application application;
I removed insertable = false, updatable = false and added optional=false
That worked
#JoinColumn(name = "application_id", optional = false)
Try this:
Apartement apartement1 = new Apartement()
Apartement apartement2 = new Apartement()
Wrapper wrapper = new Wrapper(new Application());
Application application = wrapper.getApplication();
application.getOwnedApartements().addAll(Arrays.asList(apartement1, apartement2));
apartement1.setApplicationId(application.getId());
apartement2.setApplicationId(application.getId());
WrapperRepository.saveAndFlush(wrapper);

trying to understand how #JoinTable and #JoinColumn works

I am learning hibernate and stuck a bit with the below problem
have two tables
CREATE TABLE department (
department_id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
caption varchar(255) DEFAULT NULL) ENGINE=InnoDB;
CREATE TABLE employee (
employee_id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
fio varchar(255) DEFAULT NULL,
fk_department_id int(11) NOT NULL,
FOREIGN KEY (fk_department_id) REFERENCES department (department_id)
) ENGINE=InnoDB ;
and two classes (in the first class commented out code looks like working solution)
#Entity
#Table(name = "department")
public class Department {
....
#OneToMany(cascade = CascadeType.ALL)
#JoinTable(name = "employee", joinColumns = {
#JoinColumn(name = "fk_department_id", referencedColumnName = "department_id") })
/*
* #OneToMany(fetch = FetchType.LAZY, mappedBy = "department", cascade =
* CascadeType.ALL)
*/
public Set<Employee> getEmployies() {
return employees;
}
#Entity
#Table(name = "employee")
public class Employee {
......
#ManyToOne
#JoinColumn(name = "fk_department_id")
public Department getDepartment() {
return department;
}
this results into
INFO: HHH000423: Disabling contextual LOB creation as JDBC driver reported JDBC version [3] less than 4
Exception in thread "main" org.hibernate.MappingException: Foreign key (FK3cspe1b06hmsik5l8y1i11xmd:employee [employies_employee_id])) must have same number of columns as the referenced primary key (employee [fk_department_id,employies_employee_id])
at org.hibernate.mapping.ForeignKey.alignColumns(ForeignKey.java:148)
at org.hibernate.mapping.ForeignKey.alignColumns(ForeignKey.java:130)
Please help me to understand why this doesn't work
The following should work just fine. You'll notice I am not specifying any join column relations because I am allowing Hibernate to generate those automatically for me.
#Entity
public class Department {
#OneToMany
#JoinTable(name = "department_employees")
private List<Employee> employees;
}
#Entity
public class Employee {
#ManyToOne
private Department department;
}
But lets assume you want to be explicit about the join columns.
#Entity
public class Department {
#Id
#Column(name = "department_id")
private Integer id;
#OneToMany
#JoinTable(
name = "department_employees",
joinColumns = #JoinColumn(name = "department_id"),
inverseJoinColumns = #JoinColumn(name = "employee_id"))
private List<Employee> employees;
}
#Entity
public class Employee {
#Id
#Column(name = "employee_id")
private Integer id;
#ManyToOne
#JoinTable(
name = "department_employees",
joinColumns = #JoinColumn(name = "department_id", insertable = false, updatable = false),
inverseJoinColumns = #JoinColumn(name = "employee_id", insertable = false, updatable = false))
private Department department;
}
The key points to take away from this are:
The name of the join table specifies the middle table that maintains the relationship between the Department and Employee entities. It should not refer to the Employee table as your code illustrates.
The joinColumns attribute represents the primary key attributes of the containing entity, in this case that is Department, hence I used department_id.
The inverseColumns attribute represents the primary key attributes of the associated entity, in this case that is Employee, hence I used employee_id.
Update:
If you'd like to eliminate the #JoinTable and merely maintain the relationship between Department and Employee, you'd change your mappings as follows:
#Entity
public class Department {
#OneToMany(mappedBy = "department")
private List<Employee> employees;
}
#Entity
public class Employee {
#ManyToOne
private Department department;
}
Hope that helps.

Hibernate, JPA cant delete one-to-many relation

I have one-to-many relation:
#Entity
#Table(name = "Users")
public class User {
#Id
#Column(name = "user_id", nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column(name = "login", nullable = false)
private String login;
#Column(name = "password", nullable = false)
private String password;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "role_id", nullable = false)
private Role role;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = javax.persistence.CascadeType.ALL)
private Set<Contacts> contacts = new HashSet<Contacts>();
And I'm trying to delete User object with all Contacts; I tried to use:
cascade = javax.persistence.CascadeType.ALL
cascade =
javax.persistence.CascadeType.REMOVE
#Cascade(CascadeType.DELETE) from org.hibernate.annotations
#Cascade(CascadeType.DELETE_ORPHAN) from org.hibernate.annotations
but nothing helped. I always get exception:
org.hibernate.util.JDBCExceptionReporter - Cannot delete or update a
parent row: a foreign key constraint fails
(contactmanager.contact, CONSTRAINT contact_ibfk_1 FOREIGN KEY
(user_id) REFERENCES
UPD
Code that deletes a User is as follows:
#Transactional
public void removeUser(User user) {
sessionFactory.getCurrentSession().delete(user);
}
I'll appreciate any help! Thanks.
My recommendation here would be to do the relationship management yourself. Cascading removes can be tricky (especially in a situation like yours where the owner of your bi-directional relationship is not the one declaring the cascade) and often times quite dangerous so I usually prefer to avoid them. Especially if you are running a version of JPA pre-2.0 then you don't have too much of a choice. I would just change the removal method to something like:
#Transactional
public void removeUser(User user) {
Set<Contacts> contacts = user.getContacts();
for (Contact contact : contacts) {
sessionFactory.getCurrentSession().delete(contact);
}
contacts.clear();
sessionFactory.getCurrentSession().delete(user);
}

Categories