I ran into an error with custom delete method in spring data jpa. Basically there's a bag which contains items, and when deleting the bag, all the items in it should be deleted.
Here're the entities:
#Entity
#Table(name = "bag")
public class Bag {
#Id private Long id;
#Column("uid") private Long uid;
#Column("name") private String name;
#OneToMany(mappedBy = "bag", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Item> items;
}
#Entity
#Table(name = "item")
public class Item {
#Id private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "bid", referencedColumnName = "id")
private Bag bag;
}
and the repository:
#Repository
public interface BagRepository extends JpaRepository<Bag, Long> {
Bag findByUidAndName(Long uid, String name);
#Transactional
#Modifying
#Query(value = "DELETE FROM `bag` WHERE `uid` = :uid AND `name` = :name", nativeQuery = true)
void deleteByUidAndName(#Param("uid") Long uid, #Param("name") String name);
}
When I call bagRepository.deleteByUidAndName(uid, name), I get an Exception from hibernate relating to foreign key constraint. Setting spring.jpa.show-sql=true shows it does not try to delete the items first before deleting the bag.
However, if I call Bag bag = bagRepository.findByUidAndName(uid, name) and then bagRepository.deleteById(bag.getId()) everything is fine.
I'd like to know what's wrong about customizing this delete method and how to fix it.
In case deleting entity via bagRepository.deleteById(bag.getId()) Hibernate will remove from parent to child entity because you defined cascade = CascadeType.ALL on the relation. When we perform some action on the target entity, the same action will be applied to the associated entity.
Logic is in Hibernate and does not utilize database cascades.
#OneToMany(mappedBy = "bag", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Item> items;
In case bagRepository.deleteByUidAndName(uid, name) you defined native query for deletion. This means that Hibernate logic will be ignored and the query will be executed as-is. You are working directly with the database in this case and to delete record via native SQL you need to define ON DELETE CASCADE on the database level to have similar logic.
#Query(value = "DELETE FROM `bag` WHERE `uid` = :uid AND `name` = :name", nativeQuery = true)
void deleteByUidAndName(#Param("uid") Long uid, #Param("name") String name);
Solution 1, #OnDelete(action = OnDeleteAction.CASCADE)
In case you have auto-generated tables you can add Hibernate-specific annotation #OnDelete to the relation. During tables generation ON DELETE CASCADE will be applied to the foreign key constraint.
Relation definition:
#OneToMany(mappedBy = "bag", cascade = CascadeType.ALL, orphanRemoval = true)
#OnDelete(action = OnDeleteAction.CASCADE)
private List<Item> items;
Auto generated constaint:
alter table item
add constraint FK19sn210fxmx43i8r3icevbeup
foreign key (bid)
references bag
on delete cascade
Implemetation:
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import javax.persistence.*;
import java.util.List;
#Entity
#Table(name = "bag")
public class Bag {
#Id
private Long id;
#Column(name = "uid")
private Long uid;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "bag", cascade = CascadeType.ALL, orphanRemoval = true)
#OnDelete(action = OnDeleteAction.CASCADE)
private List<Item> items;
}
Solution 2, #JoinColumn annotation with foreign key ON DELETE CASCADE
Specify foreign key with ON DELETE CASCADE for Item entity
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "bid", referencedColumnName = "id",
foreignKey = #ForeignKey(
name="FK_ITEMS_ID",
foreignKeyDefinition = "FOREIGN KEY (ID) REFERENCES ITEM(BID) ON DELETE CASCADE"))
private Bag bag;
Implementation:
import javax.persistence.*;
#Entity
#Table(name = "item")
public class Item {
#Id
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "bid", referencedColumnName = "id",
foreignKey = #ForeignKey(
name="FK_ITEMS_ID",
foreignKeyDefinition = "FOREIGN KEY (ID) REFERENCES ITEM(BID) ON DELETE CASCADE"))
private Bag bag;
}
Solution 3, do not use native query
In this case Hibernate logic will be applied.
Define repository like:
#Repository
public interface BagRepository extends JpaRepository<Bag, Long> {
Bag findByUidAndName(Long uid, String name);
#Transactional
#Modifying
void deleteByUidAndName(#Param("uid") Long uid, #Param("name") String name);
}
Solution 4, Add ON DELETE CASCADE manually to the database
In case your table is not auto-generated you can manually add ON DELETE CASCADE to the database.
alter table item
add constraint FK_BAG_BID
foreign key (bid)
references bag
on delete cascade
Related
I have 2 entities, Event and Tag, in a many-to-many relationship. Tags should have unique names, so I placed a constraint on it.
It works like expected for unique tag names, I save a batch of new events and entries are automatically inserted in the tag and join tables.
But the moment I try to save an event that has a tag with a duplicate name, an error is thrown due to it violating the constraint.
Is there a way around this that does not involve having to check and insert all the events/tags manually?
Code below:
Event entity:
#Entity
#Table(name = "event")
class Event {
#Id long id;
#ManyToMany(cascade = { CascadeType.ALL })
#JoinTable(name = "event_tag",
joinColumns = #JoinColumn(name = "event_id"),
inverseJoinColumns = #JoinColumn(name = "tag_id"))
private Set<Tag> tags;
// other properties
}
Tag entity:
#Entity
#Table(name = "tag", uniqueConstraints = #UniqueConstraint(columnNames = "name"))
public class Tag {
#Id long id;
#Column(name = "name", unique = true)
private String name;
#ManyToMany(mappedBy = "tags", cascade = { CascadeType.ALL })
private Set<Event> events;
}
I'm using JpaRepository's method saveAll to persist the events. It throws:
java.sql.SQLException: Duplicate entry 'xxxxx' for key 'uq_tag_name'
at org.mariadb.jdbc.internal.protocol.AbstractQueryProtocol.readErrorPacket(AbstractQueryProtocol.java:1694) ~[mariadb-java-client-2.7.3.jar:na]
I have seen a few similar questions but have yet to find a working answer for this.
it happen because of { CascadeType.ALL }
when you set CascadeType.ALL hibernate changes the Tag table and if it need,insert data for Tag.you should remove CascadeType.ALL and if you get (cannot insert transient object) should use flush for handlinf that
I am running into a problem deleting related entities from my database. I have a trading application where users can post trades and express their interests in other people's trades.
When a user deletes their account, all trades posted and interests expressed by this user should be removed from the database. However, the latter doesn't seem to work (I am also not sure if the first one works as I don't know in what order they get executed). I get the error:
The DELETE statement conflicted with the REFERENCE constraint "FKq9kr60l7n7h3yf82s44rkoe4g". The conflict occurred in database "dbi438161_i438161", table "dbo.interests", column 'user_id'.
Note: I get the same when I try to delete a trade but then the column is 'trade_id'
I do the same for the trades and roles of a user so I think it has to do with what is in my interest entity. I am using CascadeType.ALL annotation to let Hibernate remove related entities
Lists of related entities in user:
#ManyToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
#JoinTable(name="user_roles",
joinColumns = { #JoinColumn(name = "user_id") },
inverseJoinColumns = { #JoinColumn(name = "role_id") })
private List<Role> roles = new ArrayList<>();
#Transient
#JsonIgnore
#OneToMany(cascade=CascadeType.ALL, mappedBy="user")
private List<Interest> interests = new ArrayList<>();
#Transient
#JsonIgnore
#OneToMany(cascade=CascadeType.ALL, mappedBy="user")
private List<Trade> trades = new ArrayList<>();
Interest entity:
#Entity
#Table(name = "interests")
public class Interest {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int interestId;
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private User user;
#ManyToOne
#JoinColumn(name = "trade_id", nullable = false)
private Trade trade;
private String comment;
public Interest(User user, Trade trade, String comment) {
this.user = user;
this.trade = trade;
this.comment = comment;
}
public Interest(){
}
}
For comparison, the trade entity:
#Entity
#Table(name = "trades")
public class Trade {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="trade_id")
private int tradeId;
#Column(name="wants")
private String wants;
#Column(name="offers")
private String offers;
#Column(name="date_last_modified")
private LocalDateTime lastModified;
#ManyToOne
#JoinColumn(name = "user_id", nullable = false)
private User user;
#Transient
#JsonIgnore
#OneToMany(cascade=CascadeType.ALL, mappedBy="trade")
private List<Interest> interests = new ArrayList<>();
public Trade(String wants, String offers, User user){
this.wants = wants;
this.offers = offers;
this.user = user;
}
public Trade() {
}
}
Does anybody have an idea on what I am doing wrong here? Thanks in advance
Try to set orphanRemoval to true for the following associations:
#OneToMany(cascade=CascadeType.ALL, mappedBy="user", orphanRemoval = true)
private List<Interest> interests = new ArrayList<>();
#OneToMany(cascade=CascadeType.ALL, mappedBy="user", orphanRemoval = true)
private List<Trade> trades = new ArrayList<>();
As it is stated in the documentation:
If the child entity lifecycle is bound to its owning parent so that the child cannot exist without its parent, then we can annotate the association with the orphanRemoval attribute and dissociating the child will trigger a delete statement on the actual child table row as well.
Please also note that you should not use cascade=CascadeType.ALL for the #ManyToMany association as it explained in the documentation:
For #ManyToMany associations, the REMOVE entity state transition doesn’t make sense to be cascaded because it will propagate beyond the link table. Since the other side might be referenced by other entities on the parent-side, the automatic removal might end up in a ConstraintViolationException.
I have three entity classes.
Product, Category, and SubCategory.
A Category has a OneToMany relation with SubCategory
#Entity
#Table(name = "CATEGORY")
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "category_id")
private Long categoryId;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
#JoinColumn(name = "category_id")
private List<SubCategory> subCategories;
}
The product is assocciated with a Category and one of its SubCategories
#Entity
#Table(name = "PRODUCTS")
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = ("id"))
private Long id;
#ManyToOne
#JoinColumn(name = "category", unique = false, nullable = true, insertable = true, updatable = true)
private Category category;
#ManyToOne
#JoinColumn(name = "sub_category", unique = false, nullable = true, insertable = true, updatable = true)
private SubCategory subCategory;
}
now if I delete a Category, all its SubCatogries are deleted, but I also want the associations in Product to be updated to null. I thought of manually fetching all the products with the associated deleted Category and updating them manually, but is there a way to handle this with JPA annotations?
This update from JPA will be very inefficient in performance perspective.
Your table PRODUCTS has columns category abd sub_category which linked with correspond tables by foreign keys. Add to end of definition of each of these columns string 'ON DELETE SET NULL' and what you want will be done by database automatically.
The Product entity has a ManyToOne relationship with SubCategory that means that the SubCategory entity has a OneToMany relationship with Product. So, in the SubCategory class, where you have defined the OneToMany relationship, you need to mention the cascadetype = remove
#manytoone(cascade = cascadetype.remove)
Hope it solves your problem
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.
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);
}