JPA: Deletion with Foreign Keys in ManyToMany and ManyToOne relations? - java

I am using spring boot to model my backend and I have several foreign keys in my model structure.
I am unable to run delete operations in general and update operations when foreign keys are affected. I have tried several methods found on the internet but none seem to work.
Via SQL (SQL commands) can delete entities from book_queue_entry, copy and author_write but not author, book and book_store user due to foreign key constraints. Therefore I'd like to know what I am doing wrong in each of my models/relationships that don't work on SQL level.
Since it does not work on SQL level something with my modelling, at least as far as configuring on delete operations is fundamentally wrong. I can't point my finger at what.
Problem 1: Cannot delete a book: When a book is deleted, it should be deleted from the list of works of an author - in other words, it should be deleted from authors_write:
Author.java:
#Entity
#Setter
#Getter
#NoArgsConstructor
public class Author extends BaseEntity implements Serializable {
//stuff
#ManyToMany(cascade = CascadeType.ALL)
#JsonIgnore
#JoinTable(
name = "authors_write",
joinColumns = #JoinColumn(name = "author_id"),
inverseJoinColumns = #JoinColumn(name = "book_id"))
Set<Book> works;
}
Book.java
#Entity
#Setter
#Getter
#NoArgsConstructor
public class Book extends BaseEntity implements Serializable {
//stuff
#ManyToMany(mappedBy = "works", cascade = CascadeType.ALL)
#EqualsAndHashCode.Exclude
Set<Author> authors;
#JsonIgnore
#OneToMany(mappedBy = "book")
private List<BookQueueEntry> bookQueue;
}
Problem 2: Cannot delete Authors. When an author is deleted then all entries in authors_write with the corresponding author_id should be deleted. The classes are the same as above. Cascading does not work in this case either.
Problem 3: Cannot delete Users. When users are deleted borrower_id in copy should be nulled (I've read this does not work in JPA at all) and book_queue_entries with corresponding user_id should be deleted
Copy.java
#Entity
#Setter
#Getter
#NoArgsConstructor
public class Copy extends BaseEntity implements Serializable {
//stuff
#ManyToOne
#OnDelete(action = OnDeleteAction.CASCADE)
#JoinColumn
#EqualsAndHashCode.Exclude
private Book reference;
#ManyToOne
#JoinColumn
#EqualsAndHashCode.Exclude
private BookStoreUser borrower;
}
User.java:
public class BookStoreUser extends BaseEntity implements Serializable {
//more stuff here
#JsonIgnore
#OneToMany(mappedBy = "borrower")
private Set<Copy> booksBorrowed;
}
BookQueueEntry.java
#Entity
#Getter
#Setter
#NoArgsConstructor
#EqualsAndHashCode
public class BookQueueEntry extends BaseEntity implements Serializable {
//more stuff here
#ManyToOne(cascade = CascadeType.ALL)
#OnDelete(action = OnDeleteAction.CASCADE)
#JoinColumn(name = "user_id", nullable = false)
#EqualsAndHashCode.Exclude
private BookStoreUser user;
#ManyToOne
#OnDelete(action = OnDeleteAction.CASCADE)
#JoinColumn(name = "book_id", nullable = false)
#EqualsAndHashCode.Exclude
private Book book;
}
Once again, I believe something is wrong with how I've mapped the cascading operations. Something is wrong in a more general sense and I cant figure out what.
EDIT: I should note that #OnDelete(action = OnDeleteAction.CASCADE) did not work for me either.
EDIT 2: With a clear head, I've been able to fix Problems 1 and 2 using this: https://www.youtube.com/watch?v=vYNdjtf7iAQ

As noted in the comments, follow this video: https://www.youtube.com/watch?v=vYNdjtf7iAQ
My problems are solved but when deleting Users it throws a ConcurrentModificationException which is not really related to this problem.
My classes now look like this:
Book.java
public class Book extends BaseEntity implements Serializable {
#Column(unique = true)
private String isbn;
#NotBlank
private String title;
private Integer year;
private String imageUrl;
#ManyToMany(mappedBy = "works")
#EqualsAndHashCode.Exclude
Set<Author> authors;
private Boolean isAvailable;
private Integer version;
#Enumerated(EnumType.STRING)
private Language language;
#JsonIgnore
#OneToMany(mappedBy = "reference", cascade = CascadeType.REMOVE, orphanRemoval = true)
private Set<Copy> copies;
#JsonIgnore
#OneToMany(mappedBy = "book", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<BookQueueEntry> bookQueue;
// see here why: https://www.youtube.com/watch?v=vYNdjtf7iAQ
public void addAuthor(Author author) {
this.authors.add(author);
author.getWorks().add(this);
}
public void removeAuthor(Author author) {
this.authors.remove(author);
author.getWorks().remove(this);
}
}
Author.java:
public class Author extends BaseEntity implements Serializable {
private String name;
#ManyToMany
#JsonIgnore
#JoinTable(
name = "authors_write",
joinColumns = #JoinColumn(name = "author_id"),
inverseJoinColumns = #JoinColumn(name = "book_id"))
Set<Book> works;
public void addBook(Book book) {
this.works.add(book);
book.addAuthor(this);
}
public void removeBook(Book book) {
this.works.remove(book);
book.removeAuthor(this);
}
}
Copy.java:
public class Copy extends BaseEntity implements Serializable {
#ManyToOne
#JoinColumn
#EqualsAndHashCode.Exclude
private Book reference;
private LocalDate borrowedAt;
private LocalDate dueDate;
#ManyToOne
#JoinColumn
#EqualsAndHashCode.Exclude
private BookStoreUser borrower;
#NotNull
#Enumerated(EnumType.STRING)
private Location location;
public void addBorrower(BookStoreUser user) {
user.addBorrowedCopy(this);
}
public void removeBorrower(BookStoreUser user) {
user.removeBorrowedCopy(this);
}
}
BookStoreUser.java
public class BookStoreUser extends BaseEntity implements Serializable {
#NotNull
#Column(unique = true)
private String email;
private String firstName;
#NotNull
private String lastName;
private boolean isAdmin;
#JsonIgnore
#OneToMany(mappedBy = "borrower", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Copy> booksBorrowed;
#NotNull
private String password;
public void addBorrowedCopy(Copy copy) {
this.booksBorrowed.add(copy);
copy.setBorrower(this);
copy.setBorrowedAt(LocalDate.now());
copy.setDueDate(LocalDate.now().plusMonths(1));
}
public void removeBorrowedCopy(Copy copy) {
this.booksBorrowed.remove(copy);
copy.setBorrower(null);
copy.setBorrowedAt(null);
copy.setDueDate(null);
}
}
And also make sure to make use of the utility methods in the service layer like this:
public Long deleteById(Long id) {
this.validator.checkIDNotNull(id);
Book book = em.find(Book.class, id);
for (Author author : book.getAuthors()) {
book.removeAuthor(author);
}
bookRepository.deleteById(id);
validator.checkEntityNotExists(id);
return id;
}
public Book create(Book book) {
this.checkISBNIsValid(book.getIsbn());
this.checkISBNExists(book.getIsbn());
this.checkEntityHasValues(book);
for (Author author : book.getAuthors()) {
author.addBook(book);
}
log.info("Book with ISBN {} created successfully", book.getId());
return bookRepository.save(book);
}
EDIT: ConcurrentModificationException: Deleting from a list/Set will throw this problem. This can be found on SO - to fix ConcurrentModificationExceptions you have to create a new HashSet with whatever is throwing that problem and iterate through that:
new HashSet<Author>(book.getAuthors())
.forEach(author -> book.removeAuthor(author));

Related

Spring boot hibernate bidirectional mapping many to one can not established relationship

I have two entity table one category and other is subject
My category entity
#Entity
public class Category extends AuditableEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(unique = true)
private String name;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "description_id")
private CategoryDescription description;
#OneToMany( mappedBy = "category", cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Subject> subjects;
//getter and setter
}
And my Subject entity
#Entity
public class Subject extends AuditableEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#Column(unique = true)
private String name;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "description_id")
private SubjectDescription description;
#ManyToOne(fetch = FetchType.EAGER)
private Category category;
//Getter and Setter
}
Category Repository
#Repository
#Transactional
public interface CategoryRepository extends
JpaRepository<Category, Integer> {
}
Subject Repository
#Repository
#Transactional
public interface SubjectRepository extends JpaRepository<Subject,
Integer> {
}
Category Controller
#RestController
#RequestMapping("/category/api/")
public class CategoryResource {
private final CategoryService categoryService;
private final SubjectService subjectService;
private final TopicService topicService;
public CategoryResource(CategoryService categoryService,
SubjectService subjectService, TopicService topicService) {
this.categoryService = categoryService;
this.subjectService = subjectService;
this.topicService = topicService;
}
#PostMapping("save")
public void saveCategory(#RequestBody Category category) {
categoryService.save(category);
}
I am using postman to save data. Problem is that after saving data to the category and subject table my subject table column category_id is null i can not established a relationship between them my sql structure and data is after saving data it shows like
Category table
Subject Table
category_id is NULL how to set category id i am trying many ways but couldn't find a solution.Please help me to solve this issue
It's great that you are learning spring boot!
To answer your question since the answer is pretty simple, your code is missing category in subject.
subject.setCategory(category);
Now this might cause you an exception, so make sure you save category before you persist subject.
Cheers!

Hibernate #ManyToMany with extra columns

So I've been trying the solutions out there to map a ManyToMany relationship with extra columns but none of them is working for me and I don't know what am I doing wrong.
The Many to Many relationship is between Patient and Disease (a Patient can have multiple diseases and a Disease can be suffered by many Patients). The time attribute means "the type of the disease" (acute, chronic...)
My classes are:
#Entity
#Table(name="patient")
public class Patient{
#Id
#NotNull
#Column(name="nss")
private String NSS;
//Some attributes
#OneToMany(mappedBy = "patient")
private Set<PatientDisease> diseases = new HashSet<PatientDisease>();
//Empty constructor and constructor using fields omitted
//Getters and setters ommited
}
,
#Entity
#Table(name="disease")
public class Disease{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id")
private Integer id;
#OneToMany(mappedBy = "disease")
private Set<PatientDisease> patients = new HashSet<PatientDisease>();
//Constructors and getters and setters ommited for brevity
}
Associated class
#Entity
#Table(name = "Patient_Disease")
#IdClass(PatientDiseaseID.class)
public class PatientDisease{
#Id
#ManyToOne(fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
#JoinColumn(name = "nssPatient", referencedColumnName = "id")
private Patient patient;
#Id
#ManyToOne(fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH})
#JoinColumn(name = "diseaseID", referencedColumnName = "id")
private Disease disease;
#Column(name="time")
private String time;
//GETTERS AND SETTERS OMMITED FOR BREVETY. Constructor NOT Needed following the example
}
The id class:
#Embeddable
public class PatientDiseaseId implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "nssPatient")
private String patient;
#Column(name = "diseaseID")
private Integer disease;
//getters and setters
//hashCode and equals
}
My main app:
...
List<Diseases> diseases = sesion.createQuery("from Disease").getResultList();
System.out.println("Diseases: ");
for(Disease d: diseases) {
System.out.println(d.getName());
for(PatientDisease pd: e.getPatientDisease()) {
System.out.println(pd.getPatient().toString());
}
}
...
When running the main App I get the exception on line 5 (2nd for loop):
Exception in thread "main" org.hibernate.PropertyAccessException: Could not set field value [1] value by reflection : [class entities.PatientDisease.diseases] setter of entities.PatientDisease.diseases
I have tried some solutions here in Stack Overflow an some others that I found on the Internet, but I can't get them to work and I don't know why
Because you are using #IdClass you don't need to annotate PatientDiseaseId with #Embedded and #Column. And you have to refer to the entities.
This is what it should look like:
public class PatientDiseaseId implements Serializable {
private static final long serialVersionUID = 1L;
private Patient patient;
private Disease disease;
//getters and setters
//hashCode and equals
}

How to add data to Many to Many association with extra column using JPA, Hibernate

I have a User table and a Book table that I would like to connect.
So I created third table Borrow that has foreign key (book_id, user_id) and takenDate and broughtDate fields.
User.java
#Entity
#Table(name = "Users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String surname;
private String username;
private String email;
private String password;
#OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Borrow> borrow;
....
Book.java
#Entity
#Table(name = "Books")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String title;
private String ISBN;
private String author;
private String issuer;
private Integer dateOfIssue;
private Boolean IsRented;
#OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Borrow> borrow;
.....
Borrow.java
#Entity
#Table(name = "Borrows")
#IdClass(BorrowId.class)
public class Borrow {
private Date takenDate;
private Date broughtDate;
//lazy means it will get details of book
// only if we call GET method
#Id
#JsonIgnore
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "book_id", referencedColumnName = "id")
private Book book;
#Id
#JsonIgnore
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
....
BorowId.java
public class BorrowId implements Serializable {
private int book;
private int user;
// getters/setters and most importantly equals() and hashCode()
public int getBook() {
return book;
}
public void setBook(int book) {
this.book = book;
}
public int getUser() {
return user;
}
public void setUser(int user) {
this.user = user;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BorrowId)) return false;
BorrowId borrowId = (BorrowId) o;
return getBook() == borrowId.getBook() &&
getUser() == borrowId.getUser();
}
#Override
public int hashCode() {
return Objects.hash(getBook(), getUser());
}
}
My MySql database design looks like this:
I am trying to add data to Borrow table something like this:
EDITED
#Transactional
#PostMapping("/addUser/{id}/borrow")
public ResponseEntity<Object> createItem(#PathVariable int id, #RequestBody Borrow borrow, #RequestBody Book book){
Optional<User> userOptional = userRepository.findById(id);
Optional<Book> bookOptional = bookRepository.findById(book.getId());
if(!userOptional.isPresent()){
throw new UserNotFoundException("id-" + id);
}
User user = userOptional.get();
borrow.setUser(user);
borrow.setBook(book);
borrowRepository.save(borrow);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(borrow.getId()).toUri();
return ResponseEntity.created(location).build();
}
I have't finished it because I am not sure how :/
Any tip is appreciated!
You are almost there. You just have to keep in mind two things:
1) You have to fetch the Book via repository as well (you only fetch the User currently)
2) All three operation have to be within the same transactional context:
fetching of `User`, fetching of `Book` and save `Borrow` entity.
TIP: You can put all these inside a Service and mark it as #Transactional or mark the #Post method as #Transactional. I would suggest first option, but it is up to you.
EDIT:
Optional<Book> bookOptional = bookRepository.findById(book.getId());
Also, it seems adequate to use #EmbeddedId instead of #IdClass here as ids are actual foreign entities:
#Embeddable
public class BorrowId{
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "book_id", referencedColumnName = "id")
private Book book;
#ManyToOne
#JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
}
and then in the Borrow class:
#Entity class Borrow{
#EmbeddedId BorrwId borrowId;
...
}
and in the Post method:
BorrowId borrowId = new BorrowId();
borrowId.setUser(user);
borrowId.setBook(book);
borrow.setBorrowId(borrowId);
borrowRepository.save(borrow);

Spring data jpa #ManyToOne orphanRemoval=true doesn't delete

Situation: Got Books and categories. Each category can have n books. Want to create new, update or delete books from category. If category is empty delete it from db. Currently insert, update, and delete books.
Problem: orphanRemoval=true doesn't delete category if it is empty.
#Entity
#Table(name = "book")
#Getter
#Setter
public class BookEntity implements DBEntity, Serializable {
#Id
private Integer id;
private String bookname;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.DETACH, CascadeType.REFRESH})
#JoinColumn(name = "categoryid", referencedColumnName = "id")
private CategoryEntity categoryEntity;
public Integer getId() {
return this.id;
}
}
#Entity
#Table(name = "category")
#Getter
#Setter
public class CategoryEntity implements DBEntity, Serializable {
#Id
private Integer id;
private String categoryName;
#OneToMany(cascade = CascadeType.REMOVE, mappedBy = "categoryEntity", orphanRemoval = true)
private Set<BookEntity> bookEntitySet = new HashSet<BookEntity>(0);
}
#Transactional
public interface CategoryDAO extends CrudRepository<CategoryEntity, Integer> {
}
#Transactional
public interface BookDAO extends CrudRepository<BookEntity, Integer> {
}
Thanks for any help
p.s.
Tried:
didnt help:
Spring + JPA #OneToMany with orphanRemoval
looks stupid - deleting book from category (in case 2-n foreign key wont work):
https://hellokoding.com/jpa-one-to-many-relationship-mapping-example-with-spring-boot-maven-and-mysql/
Nothing mentioned:
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

Hibernate many to many mapping, join table with PK and extra columns

I've done the necessary changes to my models outlined here. However, I don't know what to put on my join table entity.
Note that my join table has a surrogate key , and two extra columns (date and varchar).
What I've got so far is:
User.java
#Entity
#Table (name = "tbl_bo_gui_user")
#DynamicInsert
#DynamicUpdate
public class User implements Serializable {
private String id;
private String ntName;
private String email;
private Set<GroupUser> groupUsers = new HashSet<GroupUser>(0);
// Constructors and some getters setters omitted
#OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.user", cascade=CascadeType.ALL)
public Set<GroupUser> getGroupUsers() {
return groupUsers;
}
public void setGroupUsers(Set<GroupUser> groupUsers) {
this.groupUsers = groupUsers;
}
}
Group.java
#Entity
#Table (name = "tbl_bo_gui_group")
#DynamicInsert
#DynamicUpdate
public class Group implements Serializable {
private String id;
private String groupName;
private String groupDesc;
private Set<GroupUser> groupUsers = new HashSet<GroupUser>(0);
// Constructors and some getters setters omitted
#OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.group", cascade=CascadeType.ALL)
public Set<GroupUser> getGroupUsers() {
return groupUsers;
}
public void setGroupUsers(Set<GroupUser> groupUsers) {
this.groupUsers = groupUsers;
}
}
The problem is that I don't know what to put on my join table entity. Here it is.
GroupUser.java
#Entity
#Table (name = "tbl_bo_gui_group_user")
#DynamicInsert
#DynamicUpdate
#AssociationOverrides({
#AssociationOverride(name = "pk.user",
joinColumns = #JoinColumn(name = "id")),
#AssociationOverride(name = "pk.group",
joinColumns = #JoinColumn(name = "id")) })
public class GroupUser implements Serializable {
private String id;
private User userId;
private Group groupId;
private Date dateCreated;
private String createdBy;
// constructors and getters and setters for each property
// What now? ? No idea
}
user to group would be a Many-To-Many relation. Now, you are splitting that up into Two One-To-Many Relations. Therefore your Mapping Entity simple needs to complete the Many-To-Many relation, by using Many-To-One:
public class GroupUser implements Serializable {
private String id;
#ManyToOne
private User userId;
#ManyToOne
private Group groupId;
private Date dateCreated;
private String createdBy;
}
See also this example: Mapping many-to-many association table with extra column(s) (The Answer with 38 upvotes)

Categories