Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JPA map collection of Enums

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;

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;
}

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.


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


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 and PersistentEnumSet are nearly the same)
    • You could wrap the result of EnumSet.noneOf(enumType) in your PersistenEnumSet, declare AccessType.PROPERTY and provide two access methods which use reflection to read and write the elements field
  • 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 to PersistentEnumSet and drop the extra class (... interests = new PersistentEnumSet<>(InterestsEnum.class);)
  • You must use an @AttributeOverride, as given in my example, if you've got more than one PersistentEnumSet 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 a sun.misc class
    • Providing the values array as parameter has the risk that the given values are not the correct ones
  • 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