Friday, November 19, 2010

Hibernate parent-child and annotations

Hibernate and its way of associations mapping can be a bit hard to understand for beginners. Unidirectional and bidirectional mapping, cascade save, relation owner... How to construct correctly associations between objects to get desired effect? A short introduction based on OneToMany parent-child relation.

Mapping direction

First and foremost a simple idea - the mapping direction. In unidirectional mapping only one side has got a reference to the other, eg. Parent contains a set of Child objects or Child has a Parent referrence. If both Parent and Child have references to each other, we deal with bidirectional mapping.

Database representation

To truly understand what happens on relational db side, we have to imagine how model is represented in database schema. The object model is trivial:

class Parent {
 long id;
 Set children;
}
class Child {
 long id;
 ..
}

As you can see Parent contains many Child objects. At this moment we don't define if the Child points to Parent too (if we have unidirectional or bidirectional mapping). Hibernate can store this object model in database on two ways:
    • in two tables Parent and Child, where Child table contains foreign key of Parent id,
    • or in three table, with one association table (or join table) where both Parent and Child foreign keys are stored.
    In this article we will present solution without the third table.

    Cascading and "relation owner"

    The most obscure concept is IMO difference between cascading relation, and something that I call "relation owner". Cascading is simple: if relation is cascaded, then some actions performed on one relation side will be performed to the other side. For example if Parent has cascade SAVE_UPDATE on children set, all save() or update() calls on Parent will be forwarded to related Child objects.

    What does it mean in practice? I'll show it later. In general, this means that we may not core for persistence of objects belonging to cascade relation. For example, if you have CascadeType.SAVE_UPDATE on the relation, you can add transient (newly created and not yet saved to database) object to relation, and then save parent object, what will trigger saving the child transient object.

    Relation owner

    More obscuring can be something that I call "relation owner". It is an object responsible for establishing relation between objects, what in practice means, that is responsible for setting foreign key value at the many side.

    If Parent is our relation owner, then it is setting its id as foreign key value to all Child objects at saving (is setting a relation). Adding new Child object to children set is therefore enough to establish relation. If Child is a relation owner, then adding new Child object to Parent.children set isn't enough. If Child was a relation owner, it would have a parent field, and we would have to invoke Child.setParent(parent) to establish a relation.

    Thus it is important to understand these dependences to construct models correctly.

    Examples

    The most convenient way of configuring Hibernate are annotations. I use them, although their capabilites are a bit restricted in proportion to XML mappings. For now I was able to defeat any appearing problem with annotations, but I'm really surprised with these restriction - finally both configurations produce SessionFactory, so what's the problem? A laziness of developers?

    Unidirectional mapping

    Let's start from unidirectional mapping, in which Parent holds a set of Child objects, and Child knows nothing about the Parent. Their configuration looks following:

    Parent {
     @Id
     @GeneratedValue(strategy=GenerationType.AUTO)
     long id;
     @OneToMany
     @JoinColumn(name="parentId")
     Set children = new HashSet();
     ...
    }
    Child {
     @Id
     @GeneratedValue(strategy=GenerationType.AUTO)
     long id;
     ...
    }

    Because we have mapping without association table, we need to define JoinColumn at the Parent side, pointing to foreign key column name at the Child side (Hibernate would create join table instead). We can now test the code:
    
    
    Parent parent = new Parent();
    parent.getChildren().add(new Child());
    ses.save(parent);
    ses.flush();
    
    And it is easy to predict the result: Exception org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing

    Yeah, Parent has got a reference to transient Child object and cannot be saved. To make this work we need to save Child object manually, before Parent saving. But I'll choose other solution - cascading.

    Unidirectional mapping - cascade version

    Cascading at the Parent side will cause propagation of save operation for each cascaded collection item.This we can change Parent definition:

    class Parent {
     @OneToMany
     @JoinColumn(name="parentId")
     @Cascade({CascadeType.ALL})
     Set children = new HashSet();
    }

    Voila. Everything is OK this time: 
    mysql> select * from Parent;
    +----+
    | id |
    +----+
    |  1 |
    +----+
    1 row in set (0.00 sec)
    mysql> select * from Child;
    +----+----------+
    | id | parentId |
    +----+----------+
    |  1 |        1 |
    +----+----------+
    1 row in set (0.00 sec)
    
    Pay attention that Hibernate set itself the value of foreing key in Child table, so that it points to appropriate Parent row. This is because Parent object is a relation owner in this case, and it sets relation on all dependent Child objects. Additional marking Parent as relation owner in this case is not necessary.
    
    
    Let's try now version, where Parent is not a relation owner.

    Bidirectional mapping

    If Parent was not a relation owner, then who would take this hard responsibility of establishing relation? This time it has to be a Child object. To do it, it has to have explicit relation to Parent declared. So we need to change configuration:

    class Parent { 
      @OneToMany(mappedBy="parent") 
      @Cascade({CascadeType.ALL}) 
      Set children = new HashSet(); 
    } 
    class Child { 
      @ManyToOne Parent parent; 
    }

    Our Child object contains now parent field, indicating its Parent, and Parent-side relation is marked as mappedBy="parent", what makes the Child object a relation owner. In annotations we generally use always mappedBy to mark "passive" side of relation, in XML mappings we'd use inversed="true". In our example, the JoinColumn clause dissapeared, because mappedBy enoughs to mark relation owner side, and we don't have to provide explicit foreign key field name, because we have our parent field defined in Child, and Hibernate will do it for us.
    
    
    This time after executing the same test code we receive:
    mysql> select * from Parent;
    +----+
    | id |
    +----+
    |  1 |
    +----+
    1 row in set (0.00 sec)
    mysql> select * from Child;
    +----+-----------+
    | id | parent_id |
    +----+-----------+
    |  1 |      NULL |
    +----+-----------+
    1 row in set (0.00 sec)
    Saving only Parent, which is not a relation owner this time, we haven't established relation this time. But cascade save has worked anyway. To set relation we have to set parent field on Child object explicite. In practice you can define following method in Parent:
    
    
    class Parent {
     public void addChild(Child child) {
      child.setParent(this);
      getChildren().add(child);
     }
    }
    
    
    
    What causes, that at adding new Child object to Parent set, Child object will be notified about changes and will properly establish the relation.


    Bidirectional mapping - parent as relation owner?

    And what to do if we'd like to have possibility of navigate along relation from both sides (from Parent to Child and from Child to Parent), and not to bother about setting Child.setParent()? This is also possible, but rarely used:
    
    
    class Parent {
     @OneToMany
     @Cascade({CascadeType.ALL})
     @JoinColumn(name="parentId")
     Set children = new HashSet();
    }
    class Child {
     @ManyToOne
     @JoinColumn(name="parentId",updatable=false,insertable=false)
     Parent parent;
    }
    
    
    
    This strange construction (in case of annotation usage) is required to achieve it. Parent doesn't have mappedBy set, so it's a relation owner and will be setting its id as foreign key value for all related Child objects. But this time we needed to name explicitly mapping column as parentId on both sides, to let Hibernate know how to join these objects (names is both classes have to be the same).

    A drawback of this situation is that both Parent and Child are now relation owners. This is because there's no possibility of excluding Child (many-size object) from being relation owner, if it has simple field of Parent type. And, as rightly manual notices: "This solution is obviously not optimized and will produce some additional UPDATE statements.".

    I love when manuals use "obviously" word, when there is unclear what is this about. But after this lecture it could be more "obvious", and in fact we have three queries in this instance:
    /* insert Parent */ 
    insert into Parent values ( )
    /* insert Child */ 
    insert into Child values ( )
    /* create one-to-many row Parent.children */ 
    update Child set parentId=? where id=?
    
    Links

      6 comments:

      1. This is a superb introduction - thank you so much!!!

        --- cheerio atul

        ReplyDelete
      2. Copied from hibernate's official docs... huh?

        ReplyDelete
      3. Thanks, really great explanation. Helped me very much.

        ReplyDelete
      4. One many does not have any join column. Your concept is wrong

        ReplyDelete
        Replies
        1. Hibernate supports this, maybe JPA not, but this is about Hibernate.

          Delete

      Note: Only a member of this blog may post a comment.