September 5, 2013

Mapping enums with a fixed ID in JPA - An alternative to String and Ordinal

There are currently 2 ways you can map enums within your JPA entities using the @Enumerated annotation. Unfortunately both EnumType.STRING and EnumType.ORDINAL have their limitations.

If you use EnumType.String then renaming one of your enum types will cause your enum value to be out of sync with the values saved in the database. If you use EnumType.ORDINAL then deleting or reordering the types within your enum will cause the values saved in the database to map to the wrong enums types.

Both of these options are fragile. If the enum is modified without performing a database migration, you could jeopodise the integrity of your data.

A possible solution is to use the JPA lifecycle call back annotations, @PrePersist and @PostLoad. This feels quite ugly as you will now have two variables in your entity. One mapping the value stored in the database, and the other, the actual enum.

The preferred solution is to map your enum to a fixed value, or ID, defined within the enum. Mapping to predefined, fixed value makes your code more robust. Any modification to the order of the enums types, or the refactoring of the names, will not cause any adverse effects.

If you are using JPA 2.1 you have the option to use the new @Convert annotation. This requires the creation of a converter class, annotated with @Converter, inside which you would define what values are saved into the database for each enum type. Within your entity you would then annotate your enum with @Convert.

The reason why I prefer to define my ID's within the enum as oppose to using a converter, is good encapsulation. Only the enum type should know of its ID, and only the entity should know about how it maps the enum to the database.

Here is an example:

public class Player {

    @Id
    @Column(name="player_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    //...

    private Integer position;

    public Position getPosition() {
        return Position.getType(this.position);
    }

    public void setPosition(Position position) {

        if (position == null) {
            this.position = null;
        } else {
            this.position = position.getId();
        }
    }
}


public enum Position {

    FORWARD(1),
    DEFENCE(2),
    MIDFIELD(3),
    GOALKEEPER(4);

    private int id;   

    private Position(int id) {
        this.id = id;
    }

    public static Position getType(Integer id) {
      
        if (id == null) {
            return null;
        }

        for (Position position : Position.values()) {
            if (id.equals(position.getId())) {
                return position;
            }
        }
        throw new IllegalArgumentException("No matching type for id " + id);
    }

    public int getId() {
        return id;
    }
}

Note: by using an integer data type rather than a String data type, will mean quicker database queries when using the value in where clause. While this may not matter in smaller datasets of say 10,000 records, much larger datasets may notice a reduction in performance.


11 comments :

  1. So, You don't recommend saving position description along with it's id into database for future queries ease?

    ReplyDelete
    Replies
    1. I used to think this. From a database point of view, it it more correct to have the enum ID's, values and descriptions in a look up table, but from the Java side, it is quicker and simpler to use the enum directly. Plus there is no need for the extra join in the database.

      The down side to this is that if you are querying the database directly, you need to know which ID (example above - positionId in player) means what, and for this you will need to look at the Java enum.

      Delete
    2. Thanks a lot, your replay made me look for a better way to implement enums in JPA, I ended up implementing converters, their funcionality is exatly the one I was looking for.

      Delete
  2. I have seen several implementations of this, may using a hashmap for reverse mapping?

    ReplyDelete
  3. I don't really understand your question, could you clarify?

    ReplyDelete
  4. To answer Thor's question, in the enum put the following code:
    private final static Map<Integer, Position> intToEnum = new HashMap<>();
    static {
    for (Position pos : values())
    intToEnum.put(pos.id, pos);
    }
    public static Position fromInteger(Integer id) {
    return intToEnum.get(id);
    }

    This is from Effective Java, 2nd ed, item 30, page 154.

    I'm using this with the @Converter and all the converter methods do is call those enum methods; 1 line or so of code in each method.

    ReplyDelete
    Replies
    1. By "all the converter methods do is call those enum methods" I meant the fromInteger() and getId(). You'd might prefer to rename fromInteger() to fromId() for consistency.

      Delete
    2. Hi, I find the lumpynose's approach interesting as the converter will only be as followed

      @Converter
      public class PositionConverter implements AttributeConverter {
      @Override
      public Integer convertToDatabaseColumn(final Position attribute) {
      return attribute.getId();
      }

      @Override
      public Position convertToEntityAttribute(final String dbData) {
      return Position.fromInteger(dbData);
      }
      }

      Delete
  5. "Note: by using an integer data type rather than a String data type, will mean quicker database queries"

    Not always true. char (1 byte) is more space-efficient (both in disk and RAM) and will be more performant than smallint (2 bytes) and integer (4 bytes), as most enums have no more than 255 distinct values. Your example uses 4 values in a space of 4 bytes, it fits within a char and even a smallint. As enums are usually indexed this matters even more. For non-indexed enums this will matter much less.

    ReplyDelete