Adding columns to join tables (in the context of JPA/Hibernate)

At some point in a @ManyToMany relationship I had to add some extra columns in the join table (the middle table).

Here's what Gavin King says in Java Persistence with Hibernate (a notable book on the subject):

Adding columns to join tables
You can use two common strategies to map such a structure to Java
classes. The first strategy requires an intermediate entity class for
the join table and is mapped with one-to-many associations. The second
strategy utilizes a collection of components, with a value-type class
for the join table.

Later in that chapter for the first approach (the extra entity for the middle table):

The primary advantage of this strategy is the possibility for
bidirectional navigation: You can get all items in a category {...} and
the also navigate from the opposite direction {...}. A disadvantage is
the more complex code needed to manage {...} entity instances to create
and remove associations—they have to be saved and deleted
independently, and you need some infrastructure, such as the composite
identifier. However, you can enable transitive persistence with
cascading options on the
collections {...}, as explained {...}, “Transitive persistence.”

Later in that chapter for the second approach (the collection of components approach):

That’s it: You’ve mapped a ternary association with annotations. What looked
incredibly complex at the beginning has been reduced to a few lines of annota-
tion metadata, most of it optional.

Naively enough I chose the second approach. Who cares that there's a
hibernate dependency in my JPA data access layer. I already have a few
(a hibernate interceptor).

In this approach I had to use the @CollectionOfElements annotation. @CollectionOfElements
works like that: it maps a collection (set, map, list) of something to
a table. This table has no entity attached to it. It can work with
value types, Strings and @Embeddables. In my case it had to be the @Embeddable.

Let me give you an example - it will clear things up: there are classes
and there are students - two entities. There can be two classes with
many students some of which are the same - so the relationship is @ManyToMany. The extra column in the join table would the grade of the student in that class.

So the approach with the @CollectionOfElements works like that: one of the entities holds the relationship - let it be the class entity - so it has something like that:


@Entity


public class
Class {
    @Version
    private int version;
    ...
    @org.hibernate.annotations.CollectionOfElements
    private Set<GradedStudent> students;
    ...
}

Student is a simple entity, no code needed. Let's call the student with the grade an GradedStudent:


@Embeddable


public class
GradedStudent {
    ...
    @OneToOne(..., cascade = {MERGE, PERSIST, REFRESH})
    private Student student;
    ...
    @Column( nullable = false, ... )
    private int grade;
    ...
}

That's pretty much it. Seems simple, you would think and straightforward.

BUT IT DOESN'T WORK.

Here's what gets wrong:

  1. Everytime a class entity gets queried, it's version gets incremented. This makes updating a disconnected entity far more difficult and makes the @Version kind of obsolete.
    Solution: none, I couldn't find anything remotely connected to this problem on the net.
  2. The primary key in the join table (with a name like 'class_gradedstudent') is not the [class_id, student_id] but is [class_id,
    student_id, grade]. If you put extra columns in the join table and they
    are nullable = false, they would become part of the primary key.
  3. Cascading fails. You have to create and persist a Student first in order it to become a part of a certain class entity. Even though a GradedStudent is said to cascade a Student.
    Solution: none, I tried everything I could think of - no luck. I
    couldn't find anything remotely connected to this problem on the net.

Regarding 2: a quotation from the same book:

There is only
one change to the database tables: The {...} table now has a primary
key that is a composite of all columns, not only the ids of the two object, as in
the previous section. Hence, all properties should never be nullable—otherwise
you can’t identify a row in the join table.

Well, what if I don't want that? It doesn't say.

So, actually the second approach is not an option.

5 thoughts on “Adding columns to join tables (in the context of JPA/Hibernate)”

  1. Hi Mihaill, I used this example but I didn't achieve done. When I tried to load the application, its was generated the following error :
    Caused by:
    org.hibernate.MappingException: Could not determine type for: com.provider.habita.benfeitoria.model.Benfeitoria, at table: HBTA.TBIMOVELHABITA, for columns: [org.hibernate.mapping.Column(benfeitoria)]
    at org.hibernate.mapping.SimpleValue.getType(SimpleValue.java:292)
    at org.hibernate.tuple.PropertyFactory.buildStandardProperty(PropertyFactory.java:143)
    at org.hibernate.tuple.component.ComponentMetamodel.<init>(ComponentMetamodel.java:68)
    at org.hibernate.mapping.Component.buildType(Component.java:175)
    at org.hibernate.mapping.Component.getType(Component.java:168)
    at org.hibernate.mapping.SimpleValue.isValid(SimpleValue.java:276)
    at org.hibernate.mapping.Property.isValid(Property.java:207)
    at org.hibernate.mapping.PersistentClass.validate(PersistentClass.java:458)
    at org.hibernate.mapping.RootClass.validate(RootClass.java:215)
    at org.hibernate.cfg.Configuration.validate(Configuration.java:1135)
    at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1320)
    at org.hibernate.cfg.AnnotationConfiguration.buildSessionFactory(AnnotationConfiguration.java:867)
    at org.springframework.orm.hibernate3.LocalSessionFactoryBean.newSessionFactory(LocalSessionFactoryBean.java:798)
    at org.springframework.orm.hibernate3.LocalSessionFactoryBean.buildSessionFactory(LocalSessionFactoryBean.java:738)

    Could you help me?

    Thank you in advance.
    Jadiael Júnior

  2. Hi Mihail, the comment above is with wrong stack trace. sorry. The correct is this:

    Hi Mihaill, I used this example but I didn't achieve done. When I tried to load the application, its was generated the following error :
    Caused by:
    org.hibernate.MappingException: Could not determine type for: java.util.Set, at table: HBTA.TBIMOVELHABITA, for columns: [org.hibernate.mapping.Column(benfeitoriasDoImovel)]
    at org.hibernate.mapping.SimpleValue.getType(SimpleValue.java:292)
    at org.hibernate.mapping.SimpleValue.isValid(SimpleValue.java:276)
    at org.hibernate.mapping.Property.isValid(Property.java:207)
    at org.hibernate.mapping.PersistentClass.validate(PersistentClass.java:458)
    at org.hibernate.mapping.RootClass.validate(RootClass.java:215)
    at org.hibernate.cfg.Configuration.validate(Configuration.java:1135)
    at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1320)
    at org.hibernate.cfg.AnnotationConfiguration.buildSessionFactory(AnnotationConfiguration.java:867)
    at org.springframework.orm.hibernate3.LocalSessionFactoryBean.newSessionFactory(LocalSessionFactoryBean.java:798)
    at org.springframework.orm.hibernate3.LocalSessionFactoryBean.buildSessionFactory(LocalSessionFactoryBean.java:738)
    at org.springframework.orm.hibernate3.AbstractSessionFactoryBean.afterPropertiesSet(AbstractSessionFactoryBean.java:131)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1057)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1024)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:421)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:245)
    Could you help me?

    Thank you in advance.

Leave a Reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail. You can also subscribe without commenting.

This site uses Akismet to reduce spam. Learn how your comment data is processed.