Orphans remain in database even with orphanRemoval=true on one-to-many relationship (JPA/Hibernate)

18,445

Solution 1

I'm using orphanRemoval=true with an unidirectional One-to-Many association without any problem.

And actually, I tested your code and the following scenario (AbstractPolicyRule implementing equals/hashCode correctly):

Category category = new Category();
AbstractPolicyRule policyRule1 = new AbstractPolicyRule("foo");

category.addToActivePolicyRules(policyRule1);
em.persist(category);
em.flush();

assertNotNull(category.getId());
assertNotNull(category.getActivePolicyRules());
assertEquals(1, category.getActivePolicyRules().size());

category.removeFromActivePolicyRules(policyRule1);
category.addToActivePolicyRules(new AbstractPolicyRule("bar"));
// category = em.merge(category); // works with or without
em.flush();
assertEquals(1, category.getActivePolicyRules().size());

just works as expected. Below the generated traces:

22:54:30.817 [main] DEBUG org.hibernate.SQL - insert into Category (id, category_name) values (null, ?)
Hibernate: insert into Category (id, category_name) values (null, ?)
22:54:30.824 [main] TRACE org.hibernate.type.StringType - binding null to parameter: 1
22:54:30.844 [main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 1
...
22:54:30.872 [main] DEBUG org.hibernate.SQL - insert into AbstractPolicyRule (id, name) values (null, ?)
Hibernate: insert into AbstractPolicyRule (id, name) values (null, ?)
22:54:30.873 [main] TRACE org.hibernate.type.StringType - binding 'foo' to parameter: 1
22:54:30.874 [main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 1
...
22:54:30.924 [main] DEBUG org.hibernate.SQL - update AbstractPolicyRule set category_policy_id=? where id=?
Hibernate: update AbstractPolicyRule set category_policy_id=? where id=?
22:54:30.927 [main] TRACE org.hibernate.type.LongType - binding '1' to parameter: 1
22:54:30.928 [main] TRACE org.hibernate.type.LongType - binding '1' to parameter: 2
22:54:30.929 [main] DEBUG o.h.p.c.AbstractCollectionPersister - done inserting collection: 1 rows inserted
22:54:30.929 [main] DEBUG org.hibernate.jdbc.AbstractBatcher - Executing batch size: 1
...
22:54:30.945 [main] DEBUG org.hibernate.SQL - insert into AbstractPolicyRule (id, name) values (null, ?)
Hibernate: insert into AbstractPolicyRule (id, name) values (null, ?)
22:54:30.948 [main] TRACE org.hibernate.type.StringType - binding 'bar' to parameter: 1
22:54:30.948 [main] DEBUG o.h.id.IdentifierGeneratorHelper - Natively generated identity: 2
...
22:54:30.990 [main] DEBUG org.hibernate.SQL - update AbstractPolicyRule set category_policy_id=null where category_policy_id=? and id=?
Hibernate: update AbstractPolicyRule set category_policy_id=null where category_policy_id=? and id=?
22:54:30.991 [main] TRACE org.hibernate.type.LongType - binding '1' to parameter: 1
22:54:30.992 [main] TRACE org.hibernate.type.LongType - binding '1' to parameter: 2
22:54:30.993 [main] DEBUG o.h.p.c.AbstractCollectionPersister - done deleting collection rows: 1 deleted
22:54:30.993 [main] DEBUG o.h.p.c.AbstractCollectionPersister - Inserting rows of collection: [com.stackoverflow.q3304092.Category.activePolicyRules#1]
22:54:30.994 [main] DEBUG org.hibernate.jdbc.AbstractBatcher - Executing batch size: 1
...
22:54:30.996 [main] DEBUG org.hibernate.SQL - update AbstractPolicyRule set category_policy_id=? where id=?
Hibernate: update AbstractPolicyRule set category_policy_id=? where id=?
22:54:30.997 [main] TRACE org.hibernate.type.LongType - binding '1' to parameter: 1
22:54:30.998 [main] TRACE org.hibernate.type.LongType - binding '2' to parameter: 2
22:54:31.002 [main] DEBUG o.h.p.c.AbstractCollectionPersister - done inserting rows: 1 inserted
...
22:54:31.015 [main] DEBUG org.hibernate.SQL - delete from AbstractPolicyRule where id=?
Hibernate: delete from AbstractPolicyRule where id=?
22:54:31.017 [main] TRACE org.hibernate.type.LongType - binding '1' to parameter: 1

The first policy rule gets deleted.

If this is not representative of what you're doing, you should maybe provide more code.

Update: Answering a comment from the OP...

Wow I just changed the saveOrUpdate call to merge and now it's removing appropriately. You have any insight why that is?

Just a guess: since orphanRemoval is a JPA thing, I wonder if saveOrUpdate will deal appropriately with it (actually, I thought you were using the EntityManager API since you mentioned JPA).

Solution 2

First make sure your classes implement the hashCode() and equals() methods, so that hibernate knows that exactly these items are removed from the set.

Then try defining the hibernate @Cascade annotation, specifying the delete-orphan cascade type there and observer whether the same happens. If it works the way you want it - report a bug in hibernate and temporarily use the proprietary annotation. Otherwise - update the question with the details

Share:
18,445
Josh
Author by

Josh

Software developer in Atlanta.

Updated on June 03, 2022

Comments

  • Josh
    Josh almost 2 years
    @Entity
    @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
    @Table(name = "company_policies")
    @DiscriminatorColumn(name = "rule_name")
    public abstract class AbstractPolicyRule implements Serializable {
    
      @Transient
      private static final long serialVersionUID = 1L;
    
      @Id
      @GeneratedValue
      private Long id;
      private String value;
    
      ...
    }
    

    _

    @Entity
    public class Category implements Serializable {
    
      @Transient
      private static final long serialVersionUID = 1L;
    
      @Id
      @GeneratedValue
      private Long id;
      @Column(name = "category_name")
      private String name;
    
      @OneToMany(fetch = FetchType.EAGER, cascade = { CascadeType.ALL }, orphanRemoval = true)
      @JoinColumn(name = "category_policy_id", referencedColumnName = "id")
      private Set<AbstractPolicyRule> activePolicyRules;
    
      ...
    }
    

    When this Set is updated the existing activePolicyRules have their category_policy_id set to null in the database and new ones are inserted. I'd like for the original ones to be deleted.

    I thought adding the orphanRemoval = true would do that but it's not. Other questions I've seen on this appear to have bi-directional relationships and setting the parent to null solves it, but this is not a bi-directional relationship.

    Any suggestions?

    Using Hibernate 3.5.3

    Edit: This only happens when an existing AbstractPolicyRule exists in the database, I remove it from the list and then save the Category again. It's foreign key, category_policy_id, is set to null instead of being deleted.

    [DEBUG] Collection found: [domain.category.Category.activePolicyRules#1], was: 
    [<unreferenced>] (initialized)
    [DEBUG] Flushed: 0 insertions, 2 updates, 0 deletions to 2 objects
    [DEBUG] Flushed: 1 (re)creations, 0 updates, 1 removals to 1 collections
    ...
    [DEBUG] Deleting collection: [domain.category.Category2.activePolicyRules#1]
    [DEBUG] about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
    [DEBUG] update company_policies set category_policy_id=null where category_policy_id=?
    [DEBUG] done deleting collection
    

    Also tried a join table since the Hibernate documentation discourages the previous way:

    @Entity
    public class Category implements Serializable {
    
      @Transient
      private static final long serialVersionUID = 1L;
    
      @Id
      @GeneratedValue
      private Long id;
      @Column(name = "category_name")
      private String name;
    
      @OneToMany(fetch = FetchType.EAGER, cascade = { CascadeType.ALL }, orphanRemoval = true)
      @JoinTable(name = "policy_rule_mapping", 
        joinColumns = @JoinColumn(name = "category_id"), 
        inverseJoinColumns = @JoinColumn(name = "rule_id"))
      private Set<AbstractPolicyRule> activePolicyRules;
    
      ...
    }
    

    This has the same issue. The row in the mapping table is deleted but the AbstractPolicyRule still contains the removed item.