JPA map collection of Enums
Solution 1:
using Hibernate you can do
@CollectionOfElements(targetElement = InterestsEnum.class)
@JoinTable(name = "tblInterests", joinColumns = @JoinColumn(name = "personID"))
@Column(name = "interest", nullable = false)
@Enumerated(EnumType.STRING)
Collection<InterestsEnum> interests;
Solution 2:
The link in Andy's answer is a great starting point for mapping collections of "non-Entity" objects in JPA 2, but isn't quite complete when it comes to mapping enums. Here is what I came up with instead.
@Entity
public class Person {
@ElementCollection(targetClass=InterestsEnum.class)
@Enumerated(EnumType.STRING) // Possibly optional (I'm not sure) but defaults to ORDINAL.
@CollectionTable(name="person_interest")
@Column(name="interest") // Column name in person_interest
Collection<InterestsEnum> interests;
}
Solution 3:
I was able to accomplish this in this simple way:
@ElementCollection(fetch = FetchType.EAGER)
Collection<InterestsEnum> interests;
Eager loading is required in order to avoid lazy loading inizializing error as explained here.
Solution 4:
tl;dr A short solution would be the following:
@ElementCollection(targetClass = InterestsEnum.class)
@CollectionTable
@Enumerated(EnumType.STRING)
Collection<InterestsEnum> interests;
The long answer is that with this annotations JPA will create one table that will hold the list of InterestsEnum pointing to the main class identifier (Person.class in this case).
@ElementCollections specify where JPA can find information about the Enum
@CollectionTable create the table that hold relationship from Person to InterestsEnum
@Enumerated(EnumType.STRING) tell JPA to persist the Enum as String, could be EnumType.ORDINAL
Solution 5:
I'm using a slight modification of java.util.RegularEnumSet to have a persistent EnumSet:
@MappedSuperclass
@Access(AccessType.FIELD)
public class PersistentEnumSet<E extends Enum<E>>
extends AbstractSet<E> {
private long elements;
@Transient
private final Class<E> elementType;
@Transient
private final E[] universe;
public PersistentEnumSet(final Class<E> elementType) {
this.elementType = elementType;
try {
this.universe = (E[]) elementType.getMethod("values").invoke(null);
} catch (final ReflectiveOperationException e) {
throw new IllegalArgumentException("Not an enum type: " + elementType, e);
}
if (this.universe.length > 64) {
throw new IllegalArgumentException("More than 64 enum elements are not allowed");
}
}
// Copy everything else from java.util.RegularEnumSet
// ...
}
This class is now the base for all of my enum sets:
@Embeddable
public class InterestsSet extends PersistentEnumSet<InterestsEnum> {
public InterestsSet() {
super(InterestsEnum.class);
}
}
And that set I can use in my entity:
@Entity
public class MyEntity {
// ...
@Embedded
@AttributeOverride(name="elements", column=@Column(name="interests"))
private InterestsSet interests = new InterestsSet();
}
Advantages:
- Working with a type safe and performant enum set in your code (see
java.util.EnumSet
for a description) - The set is just one numeric column in the database
- everything is plain JPA (no provider specific custom types)
- easy (and short) declaration of new fields of the same type, compared with the other solutions
Drawbacks:
- Code duplication (
RegularEnumSet
andPersistentEnumSet
are nearly the same)- You could wrap the result of
EnumSet.noneOf(enumType)
in yourPersistenEnumSet
, declareAccessType.PROPERTY
and provide two access methods which use reflection to read and write theelements
field
- You could wrap the result of
- An additional set class is needed for every enum class that should be stored in a persistent set
- If your persistence provider supports embeddables without a public constructor, you could add
@Embeddable
toPersistentEnumSet
and drop the extra class (... interests = new PersistentEnumSet<>(InterestsEnum.class);
)
- If your persistence provider supports embeddables without a public constructor, you could add
- You must use an
@AttributeOverride
, as given in my example, if you've got more than onePersistentEnumSet
in your entity (otherwise both would be stored to the same column "elements") - The access of
values()
with reflection in the constructor is not optimal (especially when looking at the performance), but the two other options have their drawbacks as well:- An implementation like
EnumSet.getUniverse()
makes use of asun.misc
class - Providing the values array as parameter has the risk that the given values are not the correct ones
- An implementation like
- Only enums with up to 64 values are supported (is that really a drawback?)
- You could use BigInteger instead
- It's not easy to use the elements field in a criteria query or JPQL
- You could use binary operators or a bitmask column with the appropriate functions, if your database supports that