Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly implement a owner-owned-owner2 association in java with hibernate?

I have looked for information on how to implement the following association in hibernate and although the hibernate manual is very thorough, I haven't found the following use case addressed.

I have a requirement to have an association between to entities, where the association has several attributes besides the foreign keys to the associated entities. The specifications for the relation are:

  • A Container is associated to Contained through Position.
  • A Position can not exist without both a Container and a Contained item.
    • Hence, if either the Container or the Contained item is deleted, the Position should be deleted.
  • A Container can contain 0 or more Positions.
  • A Position refers to one and only one Contained item.

I have managed to configure most of the requirements through annotations and it works out quite nicely, except for cascading the delete from the Contained item. I have a work around to do this described below, but I would like to configure this action through annotations to have the database automatically do the work in order to have a more robust referential integrity.

This is the mapping that I have so far:

@Entity
public class Container
{
    @OneToMany(cascade = CascadeType.ALL,
               orphanRemoval = true,
               fetch = FetchType.EAGER)
    @JoinColumn(name = "container_fk")
    public Set<Position> getPositions() { return this.positions; }
    public void setPositions(final Set<Position> positions) { this.positions = positions; }
    private Set<Position> positions;
    ...
}

@Entity
public class Position
{
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "container_fk", insertable = false, updatable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    public Container getContainer() { return this.container; }
    public void setContainer(Container container) { this.container = container; }
    private Container container;

    @NaturalId
    @OneToOne(optional = false, fetch = FetchType.EAGER)
    @JoinColumn(name = "contained_fk")
    public Contained getContained() { return this.contained; }
    public void setContained(Contained contained) { this.contained = contained; }
    private Contained contained;

    // other attributes are owned by this relationship
    // (i.e., they don't make sense in either Container or Contained.
    ...
}

To delete the Position, when deleting the Contained item the following is implemented in code in a ContainedDao (presented without exception handling and session management is done in the base Dao class for simplicity):

@Repository
@Transactional(rollbackFor = Throwable.class)
public class ContainedDao extends TransactionalDao<Contained>
{
    public void delete(String id)
    {
        final Session session = getSession();

        // If there is a Position associated to the Contained item delete it,
        // and remove it from any Container collection.
        Position position = (Position) session.createCriteria(Position.class)
                                              .createCriteria("contained")
                                              .add(Restrictions.eq("id", id))
                                              .uniqueResult();
        if (position != null)
        {
            position.getContainer().getPositions().remove(position);
            session.delete(position);
        }

        // Delete the Contained item.
        Contained object = session.load(Contained.class, id);
        session.delete(contained);
    }
}

What I would like to do is to somehow configure through annotations so that the ContainedDao.delete method is simplified to a simple:

// Delete the Contained item.
Contained object = session.load(Contained.class, id);
session.delete(contained);

Is this possible? Or is my current solution the best I can get? Is there a better way to approach this? Note that a key factor here is that Position containes additional attributes; otherwise, I would have configured the association between Container/Contained directly and let hibernate manage the association.

like image 846
miguelf Avatar asked Nov 13 '22 17:11

miguelf


1 Answers

When you say "A Position refers to one and only one Contained item", do you mean a one-to-one relationship (as opposed to a many-to-one)? If that is the case, then I suppose what you really want to express here is that Position is-a Contained entity.

The single difference between a one-to-one and an is-a relationship is in the life-cycle of the relationship. In a one-to-one relationship the Contained instance to which the Position points to might change. An is-a relationship is more restricted: the relationship is essentially the identity of the Position itself, therefore changing it is not allowed.

To implement an is-a relationship you need three things:

  1. The foreign-key from Position to Contained should also be the primary-key of Position. Since primary keys don't change (I am allowing myself the luxurious presumption here that you are using an auto-generated surrogate key on Contained) the is-a relationship cannot be updated either. When you delete a row from Contained, you have to remove any referencing Positions too (there can be at most one).
  2. The Position class should extend the Contained class. As above with the database model, an extends declaration will stay as such for the whole life-cycle of a Position object. When you delete the Contained object, your Position is gone too (they are the same object in memory, only accessed through different types of references)
  3. In your Hibernate configuration specify Position to be a joined-subclass of Contained.

Then following DAO code to delete a Contained instance would also cascade to Position:

public void deleteContained(String id)
{
    Contained c = (Contained) session.createCriteria(Contained.class)
                                     .add(Restrictions.eq("id", id))
                                     .uniqueResult();

    // Removes associated Position too, if exists
    session.delete(c);
    // This would fail now:
    // session.load(Position.class, id);
}

Also, deleting a Position instance would cascade to Contained:

public void deletePosition(String id)
{
    Position p = (Position) session.createCriteria(Position.class)
                                   .add(Restrictions.eq("id", id))
                                   .uniqueResult();

    // Removes associated Contained too
    session.delete(p);
    // This would fail now:
    // session.load(Contained.class, id);
}

P.S. A one-to-one relationship is a bogus concept anyway, an OOP illusion. Instead of using a one-to-one, such relationships should either be properly restricted to an is-a, or reconsidered as a many-to-one. You can substitute most of my points about one-to-one with many-to-one above.

like image 73
Daniel Dinnyes Avatar answered Dec 17 '22 16:12

Daniel Dinnyes