@OneToMany Fixes in JPA 2
December 27th, 2009 by SamJPA 2 has been released as a part of J2EE 6. Along with many welcome new features, JPA2 has addressed several oversights in @OneToMany Collection mapping, including:-
- Delete Orphans - where
mappedByentities would persist when deleted. Collections of primitive, core or@Embeddabletypes - not allowed in JPA 1.
This post shows how Hibernate currently works around these oversights and how the JPA 2 annotations will replace the need for workarounds.
Rock solid implementations of JPA 2 are not yet present, but it will not be long before a stable version of Hibernate 3.5 is available. Be warned that EclipseLink 2.0.0 does not support optional = false and Hibernate 3.5.0 Beta 2 does not support orphanRemoval = true. Please vote/comment on the linked issue reports in order to ensure they are fixed with priority.
Delete Orphans
In JPA, one way to have a Collection as a field in an @Entity is to use the @OneToMany annotation. “One To Many” means that there are many collection elements for this one entity, and that the collection elements are not a part of any other entity’s collection.
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<SomeEntity> resultSet = Sets.newHashSet();
The CascadeType defines the operations that are propagated to the associated entity. FetchType defines strategies for fetching data from the database. Note that Sets.newHashSet is a convenience method from Google Collections.
The code above will most likely result in the JPA implementation creating three tables - one for the @Entity itself, one for SomeEntity and a third one to record which SomeEntitys are collected by the @Entity.
The alternative is to use a mappedBy field, like so
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "mapId")
private Set<SomeEntity> resultSet = Sets.newHashSet();
and ensure that the SomeEntity contains a @ManyToOne field called mapId that links back to the containing Entity. However, up until J2EE 6 there was a massive bug which meant that if you did this and later deleted any SomeEntity from the Set then the database would never know about it.
Now, there is a fix - J2EE 6 introduced a new field in the @OneToMany annotation called orphanRemoval which is false by default and therefore will be a continual source of bugs for years to come due to backwards compatibility with existing bugs. The recommended way to hold a collection is now
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "mapId", orphanRemoval = true)
private Set<SomeEntity> resultSet = Sets.newHashSet();
and in the SomeEntity, the @ManyToOne field should have the optional flag set to false
@ManyToOne(optional = false)
private MyEntity entity;
This is equivalent to the Hibernate-specific workaround to use
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "mapId")
@org.hibernate.annotations.Cascade(value=org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
private Set<SomeEntity> resultSet = Sets.newHashSet();
with
@ManyToOne
@JoinColumn(nullable = false)
private MyEntity entity;
The nullable join will remind you to always update this field - otherwise you’ll get an exception.
When the JPA 2 implementations start hitting the streets, you should feel safe to write data access code such as
EntityManager em = ...
em.getTransaction().begin();
try {
MyEntity entity = em.merge(existingEntity);
entity.getMySet().clear(); // removes the existing SomeEntitys from the DB
entity.getMySet().addAll(mySet); // mySet can be unmanaged entities up to now
for (SomeEntity someEntity : mySet) {
// Don't forget to set the bi-directional mapping
someEntity.setEntity(entity);
}
em.getTransaction().commit();
} catch (PersistenceException e) {
if (em.getTransaction().isActive())
em.getTransaction().rollback();
throw e;
} finally {
em.close();
}
In the meantime, use the Hibernate workaround along with the JPA 2 annotations. If anyone has equivalent EclipseLink/TopLink workarounds, please post them in the comments.
Note that there is no equivalent for @ManyToMany Collections - managing orphans is very much a manual effort in that case.
Collections of Primitive Types
The JPA 1 annotations for creating Collections had restricted the type of the contained objects to be @Entitys. If you needed a Collection of primitive/core types - e.g. String, Long, Date - or your own custom @Embeddable type, then you had to turn to vendor specific extensions such as Hibernate. With Hibernate annotations, the solution is simple - declare your fields with a @org.hibernate.annotations.CollectionOfElements annotation
@org.hibernate.annotations.CollectionOfElements
private Set<String> strings = Sets.newHashSet();
@org.hibernate.annotations.CollectionOfElements
private List<MyEmbeddable> embeddables = Lists.newArrayList();
Note that Sets.newHashSet is a convenience method from Google Collections, as is Lists.newArrayList.
The good news is that JPA 2 made it very simple and introduced @ElementCollection
@ElementCollection
private Set<String> strings = Sets.newHashSet();
@ElementCollection
private List<MyEmbeddable> embeddables = Lists.newArrayList();
Again, probably best use both the JPA 2 and Hibernate solutions until JPA 2 implementations are more widespread.
A final word - don’t forget to update your persistence.xml file to version 2.0 when using these new features.
Dave Macpherson wrote:
January 9th, 2010 at 7:00 pm