Getting Hibernate to cascade on save, but not delete? - java

So I'm having a problem with my hibernate implementation. When I try to delete a parent class, I receive a foreign key constraint exception on a class deep within the cascade hierarchy. Before I go into specifics, I'll first describe the relationships of the classes, as it has a bearing on how they need to be saved and deleted.
At the top level, I have a Customer class, which contains a list of DefaultMask objects. This is the master list, in that these default masks are used by other classes in my object hierarchy, but always from this list. Masks are only created into this list and deleted from this list.
Further down the hierarchy, I have a Column class, which can (optionally) have a DefaultMask set on it. To describe the relationship more succinctly;
A Customer OWNS zero to many DefaultMasks.
A Customer OWNS zero to many Columns.
A Column may have one DefaultMask.
In my application, when I attempt to delete a Customer, the exception comes from the foreign-key constraint on the Column class to the DefaultMask class, and I believe the problem is incorrect settings with CascadeType. I have researched the problem and found information on an attribute called mappedBy and on using Hibernate's own CascadeType.SAVE_UPDATE (in order to prevent Hibernate trying to delete a DefaultMask held by a Column), but I will admit I am a bit lost here and could use some direct guidance. Relevant code for the classes and the actual exception message are below.
Customer:
#Entity
public class Customer {
#Id
private String id;
#OneToMany(cascade = CascadeType.ALL)
private List<DefaultMask> masks;
//(Columns are held further down in hierarchy)
Column:
#Entity
#Table(name = "WarehouseColumn")
public class Column implements Comparable<Column> {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int hibernateID;
#OneToOne
private DefaultMask mask;
DefaultMask:
#Entity
public class DefaultMask implements Mask {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int hibernateID;
private String type;
private String mask;
Exception message:
org.hibernate.exception.ConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (hibernate.WarehouseColumn, CONSTRAINT FK8BB153D994AD57D3 FOREIGN KEY (mask_hibernateID) REFERENCES DefaultMask (hibernateID))
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (hibernate.WarehouseColumn, CONSTRAINT FK8BB153D994AD57D3 FOREIGN KEY (mask_hibernateID) REFERENCES DefaultMask (hibernateID))

You're rying to delete a customer, which automatically deletes its list of default masks. But one of these masks is referenced by a column. So the database (and thus Hibernate) refuses to execute the deletion, because it would leave the column in an inconsistent state: it would reference a default mask that doesn't exist anymore.
So you have several functional choices:
leave it as it is: the customer can't be deleted because one of its masks is still referenced by a column
remove the cascade: deleting the customer will delete the customer but not its masks.
find all the columns which reference any of the default masks of the user to be deleted, and remove these columns. Then, remove the user and its default masks
find all the columns which reference any of the default masks of the user to be deleted, and set their mask fild to null. Then, remove the user and its default masks

Cascading is closely related to the concept of logical ownership.
Basically, you need to choose one of the following options:
Customer logically owns its DefaultMasks. In this case you want DefaultMasks to be deleted when you delete a Customer, therefore you need to use CascadeType.ALL. Since Column references DefaultMask, it's probably owned by Customer as well, and should be deleted to
It can be achieved by using bidirectional relationship between DefaultMask and Column with appropriate cascading, as follows:
#Entity
public class DefaultMask implements Mask {
#OneToOne(mappedBy = "mask", cascade = CascadeType.ALL)
Column column;
...
}
DefaultMasks are entities on its own, and Customer just references existing DefaultMasks. In this case you probably don't need to use cascading for this relationship at all.

Related

Can't delete a JPA entity that is part of a #ManyToOne relationship

I have two domain objects like so:
#Entity
public class Employee {
#Id
#Column(nullable = false, name = "id")
protected Integer id;
// Note: org_id is just an integer column in the database
#JoinColumn(nullable = true, name = "org_id")
#ManyToOne(targetEntity = Org.class)
private Org org;
}
...and:
#Entity
public class Org {
#Id
#Column(nullable = false, name = "id")
protected Integer id;
}
I've come to the situation in my logic where I need to make some drastic changes to what's actually saved in the database. i.e. some Orgs are getting deleted and the Employees who were in them are getting re-allocated.
The issue I have is that my program logic currently does the following:
Delete any Employees that need to be deleted via org.springframework.data.repository.delete(Iterable<? extends T> itrbl)
Delete any Orgs that need to be deleted via org.springframework.data.repository.delete(Iterable<? extends T> itrbl)
Create new/update existing Orgs via org.springframework.data.repository.save(Iterable<S> itrbl)
Create new/update existing Employees via org.springframework.data.repository.save(Iterable<S> itrbl)
The issue comes about at step 2. I get an exception like this:
org.springframework.dao.InvalidDataAccessApiUsageException:
org.hibernate.TransientPropertyValueException: object references an
unsaved transient instance - save the transient instance before
flushing : com.sample.domain.Employee.org -> com.sample.domain.Org;
nested exception is java.lang.IllegalStateException:
org.hibernate.TransientPropertyValueException: object references an
unsaved transient instance - save the transient instance before
flushing : com.sample.domain.Employee.org
-> com.sample.domain.Org
If an Org ends up with no employees I don't want to delete the Org. Likewise, if an Employee of an Org gets deleted I don't want the Org to be deleted either.
I essentially just want something that's the same as how I've got the foreign key setup in PostgreSQL on the employees table:
CONSTRAINT fk_employees_org_id FOREIGN KEY (org_id)
REFERENCES public.orgs (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE SET NULL
I've looked at the cascade options, and I'm not sure it's applicable seeing as it's not a straight parent/child relationship (and the Employee that defines the #ManyToOne relationship isn't really the parent - it's the child) and it's not bi-directional (there's no need for an Org to have a list of all of its Employees)
You don't want cascade, since you've said yourself you don't want related objects to be deleted (and that's all that cascade does).
If an Org needs to be deleted yet still has a FK pointing to it, then just null out the link to the Org in the Employee(s) ... PRIOR to delete of the Org. You can do this via a JPQL query to retrieve all Employee objects linked to a particular Org, and then null their relation field. Alternatively a Bulk Update could do it in one go (but be careful about in-memory objects since they would need refresh() calling on them to pick up this nulling of the FK).

Bidirectional association with one to many side as owner and non nullable foreign key

I'm having a problem setting up a bidirectional association with the one-to-many side as the owner. I will describe my issue using an example from the Hibernate documentation slightly modified.
#Entity
public class Troop {
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name="troop_fk") //we need to duplicate the physical information
public Set<Soldier> getSoldiers() {
...
}
#Entity
public class Soldier {
#ManyToOne()
#JoinColumn(name="troop_fk", insertable=false, updatable=false, nullable = false)
public Troop getTroop() {
...
}
I understand that in the world of databases the owner of the above relationship would be soldier since it has the column with the foreign key, however this does no make sense in real world logic since a soldier is part of a troop and a troop owns a soldier.
I would like to be able to use CascadeType.ALL to automatically save all Soldier when I persist their troop.
The hibernate documentation indicates that:
This solution is not optimized and will produce additional UPDATE statements.
eg. log
Insert Into Troop(id) Values(1)
Insert Into Soldier(id) Values(1)
Insert Into Soldier(id) Values(2)
Update Soldier Set troop_fk = 1 Where id = 1
Update Soldier Set troop_fk = 1 Where id = 2
The problem is that when we first try to insert soldier without the troop_fk an exception is thrown since that column is not nullable.
Why does Hibernate not just add the troop_fk to the insert instead of updating the record later with it?
Is there a way for me to do what I described above?
The problem is the insertable=false, updatable=false.
This is explicitly telling Hibernate to NOT update the troop_fk when the Soldier entity is inserted or updated.
But you still won't be able to insert a new Soldier entity without first associating it with a troop because the fk column is not nullable.
If you want to be able to create Soldiers without a Troop, then you need to make the association optional: #ManyToOne(optional=true).
And I would change the Troop's #OneToMany association to use the mappedBy. In the 300-odd entities in our system we've never used a #JoinColumn on the #OneToMany side.

Hibernate how delete rows with null colums in join table

i have a problem in Hibernate.
I have 2 classes:
#Entity
#Table(name="grupos")
public class Group implements Serializable{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String nombreGrupo;
#ManyToMany
#JoinTable(name="grupos_usuarios",joinColumns=#JoinColumn(name="grupo_id"),inverseJoinColumns=#JoinColumn(name="usuario_id"))
private Set<Usuario> usuarios = new HashSet<Usuario>();
and the other class..
#Entity
#Table(name="usuarios")
public class User implements Serializable{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private String nombreUsuario;
#ManyToMany(fetch = FetchType.LAZY ,mappedBy="usuarios")
private Set<Grupo> grupos = new HashSet<Grupo>();
So im using a join table.A group can have multiple users and one user can have multiple groups.The problem is when i try to delete a group
ERROR: Cannot delete or update a parent row: a foreign key constraint fails (`pfc`.`grupos_usuarios`, CONSTRAINT `FK_2pjiv067qnbhmbgjt35vogy93` FOREIGN KEY (`grupo_id`) REFERENCES `grupos` (`id`))
For example,if I delete a group,i dont have to delete the users(is the reason why im not using cascade.remove).In this case,if I delete a group, there are rows in the join table with null colums.I suppose this is the reason of the error.Is possible to delete this rows ?
Thanks
The reason for the error is explained in the exception message:
a foreign key constraint fails (pfc.grupos_usuarios, CONSTRAINT FK_2pjiv067qnbhmbgjt35vogy93 FOREIGN KEY (grupo_id) REFERENCES grupos (id))
You're trying to delete a group to which users belong. So either you decide this should not be possible (and the error is a good thing), or you decide that the users should first be removed from the group, and you have to explicitely do it before removing the group:
group.getUsuarios().clear(); // removes all the users of this group
session.delete(group);
EDIT:
To delete a user, you also need to remove the association. Since group.usuarios is the owner side of the association, that's the side that must be modified. Clearing User.grupos is not sufficient:
for (Group group : user.getGrupos()) {
group.getUsuarios().remove(user); // removes all the associations between the user and its groups
}
user.getGrupos().clear();
session.delete(user);
sounds good.but i got the same error.
maybe i will try to create two one#many relationship instead of many#many relationship.or i can use xml instead of annotations(with xml many#many works better?)i can read in internet some persons with the same problem

Hibernate Mapping One To Many Relationship:Parent's PK to Child's FK

I have a classic one to many relationship and while saving it with Hibernate, I am not able to pass parent's PK column value to Child's FK column.
Parent Class
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int holdingPK;
#OneToMany(mappedBy="holding",targetEntity=PolicyType.class,fetch=FetchType.LAZY, cascade = CascadeType.ALL)
#XmlElement(name = "Policy")
private Set<PolicyType> policy;
Child Class
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int policyPK;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="HoldingFK",nullable = false)
private HoldingType holding;
Here HoldingPKis a auto generated sequence column which represents primary key. Value gets generated when I insert a Holding row. So I want to pass HoldingPK value to child's HoldingFK column on the fly.
Test Code
HoldingType obj = new HoldingType();
obj.setCurrencyTypeCode("6");
obj.setHoldingKey("123");
Set<PolicyType> set = new TreeSet<PolicyType>();
PolicyType policy = new PolicyType();
policy.setJurisdiction("Haha");
set.add(policy);
obj.setPolicy(set);
session.save(obj);
transaction.commit();
So I am able to pass Child's other values to Child Table column, just Parent PK is not reaching to Child's FK column.
Here I am persisting XML document values to database. For this I am marshalling XML to Java Objects using JAXB then persisting objects using Hibernate. In this way I am reusing JAXB generated classes with Hibernate and these PK and FK elements do not exist on XML. These are specific to Database.
You simply forgot to initialise the owning side of the bidirectional association. You only initialized the inverse side (the one which has the mappedBy attribute). Hibernate only considers the owning side to know if an association exists or not (the side without the mappedBy attribute).
Add this to your code (before the holding is saved):
policy.setHolding(obj);
Side note: your code would be much more readable if you named the policy field (and accessors) policies. There are many of them, so it should have a plural form.

JPA composite key #OneToMany

I have the following existing DB schema, which I'd like to recreate with Java and plain JPA annotations (using hibernate as provider, so hibernate specific annotations would work as a last resort):
CREATE TABLE users (
user_id NUMBER NOT NULL -- pk
);
CREATE TABLE userdata_keys (
userdata_key_id NUMBER NOT NULL, -- pk
key VARCHAR2(128) NOT NULL
);
CREATE TABLE users_userdata (
user_id NUMBER NOT NULL, -- fk users.user_id
userdata_key_id NUMBER NOT NULL, -- fk userdata_keys.userdata_key_id
value VARCHAR2(256)
);
I've thus created the following classes and annotations:
class User {
#Id
Long id;
#OneToMany
Set<Userdata> userdata;
}
class UserdataKey {
#Id
Long id;
String key;
}
class Userdata {
String value;
#EmbeddedId
UserdataId userdataId;
}
#Embeddable
class UserdataId {
User user;
UserdataKey userdataKey;
}
I left out columnName attributes and other attributes of the entities here.
It does however not quite work as intended. If I do not specify a mappedBy attribute for User.userdata, hibernate will automatically create a table USERS_USERS_USERDATA, but as far as I've seen does not use it. It does however use the table which I specified for the Userdata class.
Since I'm rather new to Java and hibernate as well, all I do to test this currently is looking at the DB schema hibernate creates when persisting a few sample entries.
As a result, I'm entirely puzzled as to whether I'm doing this the right way at all. I read the hibernate documentation and quite a bunch of Google results, but none of them seemed to deal with what I want to do (composite key with "subclasses" with their own primary key).
The mappedBy attribute is mandatory at one of the sides of every bidirectional association. When the association is a one-to-many, the mappedBy attribute is placed ot the one- side (i.e. on the User's userdata field in your case).
That's because when an association is bidirectional, one side of the association is always the inverse of the other, so there's no need to tell twice to Hibernate how the association is mapped (i.e. which join column or join table to use).
If you're ready to recreate the schema, I would do it right (and easier), and use a surrogate auto-generated key in users_userdata rather than a composite one. This will be much easier to handle, in all the layers of your application.

Categories

Resources