9

Problem

In lack of a SQL enum type an unfortunately somewhat popular database design pattern seems to be to create one table for all enum values (thank you for the link, Nathan). I have seen many variations of this over the years, but the one that I'm currently struggling with looks something like this:

 ID  | ENUM        | VALUE
-----+-------------+----------
   1 | DAY_OF_WEEK | SUNDAY
   2 | DAY_OF_WEEK | MONDAY
    ... 
   7 | DAY_OF_WEEK | SATURDAY
    ...
  18 | PERSON_TYPE | EMPLOYEE
  19 | PERSON_TYPE | MANAGER

Which is then used like this - for example in a table of persons:

 ID | NAME     | TYPE
----+----------+------
  1 | Jane Doe | 19     

Meaning that Jane is a manger because 19 is the primary key of the person type "manager" in the enum table.

Question

Using JPA (2.1), is there an elegant way to map this construct to a propper Java enum?

Important: There are many versions of my "enum table" out in the wild with different primary key values, i.e. "manager" might sometimes be row #19, but sometimes row #231. However, the values never change at runtime. Changing the database schema is unfortunately also not an option, but using proprietary features of any JPA provider would be an option.

What worked

I actually found a solution that worked but that was too hacky for my liking:

public enum PersonType { EMPLOYEE, MANAGER }

@Table(name="PERSONS") @Entity public class Persons {
  @Id @Column(name="ID") long id;
  @Column(name="NAME") String name;
  @Convert(converter = PtConv.class) @Column(name="TYPE") PersonType type;
  // ...
}

@Converter public class PtConv implements AttributeConverter<PersonType, Integer> {
  // In a static initializer run a JDBC query to fill these maps:
  private static Map<Integer, PersonType> dbToJava;
  private static Map<PersonType, Integer> javaToDb;

  @Override public Integer convertToDatabaseColumn(PersonType attribute) {
    return javaToDb.get(attribute);
  }

  @Override public PersonType convertToEntityAttribute(Integer dbData) {
    return dbToJava.get(dbData);
  }
}

I would have lived with this if CDI was available in @Converters - but with the static construct testing was a nightmare.

Tilo
  • 3,255
  • 26
  • 31
  • 2
    I don't know why you think this is `somewhat popular`. Just use [javax.persistence.Enumerated](http://docs.oracle.com/javaee/6/api/javax/persistence/Enumerated.html) and keep your life (and that of future maintainers) simple. – Steve C Oct 16 '14 at 01:05
  • Unfortunately, the database schema wasn't designed by me. It has been around much longer than the Java application and is installed on machines of clients that are under strict audit. That's why I wrote that `Changing the database schema is unfortunately also not an option`. – Tilo Oct 16 '14 at 16:20
  • 1
    what you're describing is the "one true lookup table" https://www.simple-talk.com/sql/t-sql-programming/look-up-tables-in-sql-/ – Nathan Hughes Oct 20 '14 at 16:24
  • Thank you for the link! Good to know that even Joe Celko agrees this is bad design... – Tilo Oct 20 '14 at 17:15
  • JPA2.1 will have the converter support natively. Please go through this link (http://java.dzone.com/articles/mapping-enums-done-right). – Unknown Oct 23 '14 at 15:17

2 Answers2

1

For reference this is how I solved the problem. Proper Java enums would be my preference, and I will accept any answer that is better than this.

@Table(name="PERSONS") @Entity public class Persons {
  @Id @Column(name="ID") long id;
  @Column(name="NAME") String name;
  @Column(name="TYPE") BaseEnum type;   // known to be "PersonTypeEnum"

  public PersonType getType() {
    switch(type.getValue()) {
      case "EMPLOYEE": return PersonType.EMPLOYEE;
      case "MANAGER":  return PersonType.MANAGER;
    }
    throw new IllegalStateException(); 
  }

  public void setType(PersonTypeEnum type) {
    this.type = type;
  }
  // ...
}

@Entity @Inheritance @DiscriminatorColumn(name="ENUM") @Table(name="ENUMS")
public abstract class BaseEnum {
  @Id private int id;
  @Column(name="VALUE") String value;
  // ...
}

@Entity @DiscriminatorValue("PERSON_TYPE")
public class PersonTypeEnum extends BaseEnum { }

So the getters and the setters for enum values have different types, and setting the value requires having a reference to the entity which further blows up the code.

Tilo
  • 3,255
  • 26
  • 31
  • _Question_ - and I may be completely off - If you have a java `enum`, why would you also need a lookup table? Doesn't this mean you need to maintain two data sets redundantly? If you have an enum only and no database table to look up the enum (other than the owning entity's column value), then just stick with the value and use a [@Convert](http://www.nurkiewicz.com/2013/06/mapping-enums-done-right-with-convert.html) annotation over your attribute. Otherwise I think your solution is fine. I understand java enums are nicer to work with, but having to compile new code everytime 1/2 – coderatchet Sep 26 '17 at 02:49
  • 2/2 A change to the database enum-set occurs is cumbersome and sometimes a deal breaker. – coderatchet Sep 26 '17 at 02:50
  • @coderatchet - yes you are absolutely right. This is redundant. What I wasn't really able to express clearly in the question is that the DB schema and contents outside of my control. It seems like JPA 2.2 will allow for injections in converters, so that will solve this problem for me. – Tilo Sep 27 '17 at 17:56
0

You can use the old-fashion approach:

public final class EnumMapping {
    ... // define & load singleton data
    public int getId(Enum<?> e) { 
        ... 
    }
    public <E extends Enum<E>> E valueOf(int id, Class<E> strictCheck) { 
        ... 
        return strictCheck.cast(result);
    }
}

@Table(name="PERSONS") @Entity public class Persons {
  @Id @Column(name="ID") long id;
  @Column(name="NAME") String name;
  @Column(name="TYPE") int typeId;

  public void setPersonType(PersonType type) { this.typeId = EnumMapping.getInstance().getId(type); }
  public PersonType getPersonType() { return EnumMapping.getInstance().valueOf(typeId, PersonType.class); }
}

So you can (a) use java enums to get/set values and (b) initialize mapping at the specific moment you want.

ursa
  • 4,404
  • 1
  • 24
  • 38
  • They way I understand your suggestion, that would be just as awkward to test as the approach with the `@Converter`. Basically, it would be the very same approach and just avoiding the annotation? If I misunderstood you, can you please elaborate? – Tilo Oct 26 '14 at 18:42
  • as I understand you have 2 different tasks: (a) map int IDs to enum values and visa versa; and (b) use enums in entities instead of int IDs. the first task in your case can be solved by loading from DB (or generating during tests) and initializing an instance of EnumMapping. It can be non-final and changed for test purposes quite simple. the second task - you should have some mapping available from static context, and I suggest to use singleton (the simplest approach). YES, it is similar to @Converter approach, but you have much more freedom in initialization. – ursa Oct 26 '14 at 19:34