0

I currently have a setup similar to:

@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
    private Long id;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

@Entity
public class Container extends AbstractEntity {
    private Collection<Item> items = new HashSet<>();

    @Cascade(CascadeType.SAVE_UPDATE)
    @OneToMany(mappedBy = "container", orphanRemoval = true)
    public Collection<Item> getItems() { return items; }

    public void setItems(Collection<Item> items) { this.items = items; }
}

@Entity
public class Item extends AbstractEntity {
    private Container container;

    @ManyToOne(optional = false)
    public Container getContainer() { return container; }

    public void setContainer(Container container) { this.container = container; }
}

@Entity
public class FirstItemDetails extends AbstractEntity {
    private Item item;

    @OneToOne(optional = false)
    @Cascade({CascadeType.DELETE, CascadeType.REMOVE})
    public Item getItem() { return item; }

    public void setItem(Item item) { this.item = item; }
}

@Entity
public class SecondItemDetails extends AbstractEntity {
    private Item item;

    @OneToOne(optional = false)
    @Cascade({CascadeType.DELETE, CascadeType.REMOVE})
    public Item getItem() { return item; }

    public void setItem(Item item) { this.item = item; }
}

I've left out some of the unnecessary fields as they make no difference in the end. Now for the problem. What I want to do is something like:

public void removeItemFromContainer(Item item, Container container) {
    Transaction transaction = session.beginTransaction();
    container.getItems().remove(item);
    session.save(container);
    transaction.commit();
}

What I expect from this function is to physically remove the item from a given container and since I have orphanRemoval set to true, when I persist my container it does actually try to remove the item from database. The problem here lies with the ItemDetails entity which has a foreign_key constraint so when the commit is being ran I get:

org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: java.sql.SQLIntegrityConstraintViolationException: integrity constraint violation: foreign key no action; FK_FITEM_DETAILS_ITEM table: first_item_details
Caused by: org.hsqldb.HsqlException: integrity constraint violation: foreign key no action; FK_FITEM_DETAILS_ITEM table: first_item_details

The most confusing part to me is that when I physically add ON DELETE CASCADE in first_item_details table in database rather than relying on hibernate to cascade for me, everything works. But this approach is error prone because if at some point I'll decide to use an interceptor or eventListener for my any of my details entity it simply won't work so I'd much prefer to allow hibernate to handle it rather than needing to rely on manual db structure change.

Kęstutis
  • 1,011
  • 2
  • 12
  • 22

1 Answers1

1

You will need to make the relationship from Item to ItemDetails bidirectional. If Item doesn't know anything about this relationship why would the corresponding ItemDetail be deleted?

@Entity
public class Item extends AbstractEntity {
    private Container container;

    @OneToOne(mappedBy="item")
    @Cascade({CascadeType.DELETE, CascadeType.REMOVE})
    public ItemDetails itemDetails;
}

See a similar acceted answer here:

https://stackoverflow.com/a/7200221/1356423

While you note some issues with this, you should still be able to achieve this by using JPA Inheritance so that Item can still have a single relationship with an ItemDetails. The Inheritance Strategy will depend on your database structure:

http://en.wikibooks.org/wiki/Java_Persistence/Inheritance

Your updated code will then look like:

@Entity
@Inheritance(/*defineStrategy*/)
//define discriminator column if required
public abstract class ItemDetails extends AbstractEntity {
    private Item item;

    @OneToOne(optional = false)
    @Cascade({CascadeType.DELETE, CascadeType.REMOVE})
    public Item getItem() { return item; }

    public void setItem(Item item) { this.item = item; }
}

@Entity
//define table or discriminator depending on strategy
public class FirstItemDetails extends ItemDetails {
    //map fields specific to thus sub-class
}

@Entity
//define table or discriminator depending on strategy
public class SecondItemDetails extends ItemDetails {
     //map fields specific to thus sub-class 
}

This will also have the benefit of your not having to repeat the mapping to Item in each ItemDetails class, although it will come at the expense of an extra join if your are using a table per class.

Community
  • 1
  • 1
Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • Well, first of all, it does get deleted using standard database cascades since that's what they're for in the first place, the problem is hibernate here. Secondly, it's not really an option in my case. I do understand that it might be not that obvious from my example but the thing is that Item actually has a type and the ItemDetails entity is more like GpuDetails, CpuDetails, MotherboardDetails, MonitorDetails, etc. So I have a lot of different detail entities to eliminate redundant data and the only other way to do this was a key-value store which is slower to query. Hope this makes sense. – Kęstutis Feb 25 '15 at 15:46
  • "Well, first of all, it does get deleted using standard database cascades since that's what they're for in the first place, the problem is hibernate here"???? That's why I posted a solution relevant to Hibernate which is what you asked for. You should try it and accept as correct if it works. – Alan Hay Feb 25 '15 at 15:50
  • To use this solution it would require me to add a reference to every single detail entity available and require to alter Item database table every single time I want to add a new type which is far from an elegant solution. The problem is not if it works or not but that it's ugly as hell and impossible to maintain which is worse than no solution in the long run. To be honest if this was the only solution, I would rather delete cascade on Detail entities and simply do: `session.createQuery("from ItemDetails where item = :item").setParameter("item", cartItem).list().forEach(session::delete)` – Kęstutis Feb 25 '15 at 16:16
  • I have absolutely no idea what you are on about. – Alan Hay Feb 25 '15 at 16:33
  • Hmm, okay, I'll try to illustrate the scenario a little better. I've also modified the question to add the scenario. So, as I tried to say there can, will and is more than one ItemDetails entity, something like FirstItemDetails and SecondItemDetails but on a way bigger scale. It might be on the order of a couple of hundreds of these entities/tables so if I were to follow your suggestion and add references to every single one of them, I'd be adding hundreds of new fields to my Item entity which I hope you'll agree would simply bloat my Item entity class. – Kęstutis Feb 25 '15 at 16:50