Enum vs Code Table: Making Use of Enums
Clinical trial subjects undergo many tests for the trial. For test results, the criteria defined by CDISC SDTM are mostly used as the standard. A standard usually bundles concepts into terms and organizes them. For example, the Alpha-1 Acid Glycoprotein test has the code A1AGLP. There are a great many kinds of tests, sometimes as many as around three thousand.
While working, I suddenly found myself wondering whether this much code information is better kept as an enum or as a code table in the database.
Heap Memory
At first I worried about memory. I expected that defining this many enum constants in a single class would take up that much more memory in the method area and the heap. But remembering that a JSON dataset object of about thirty thousand entries I had recently processed was around 2 MB, I decided there was no need to worry about it.
Time Complexity
A code table is fast thanks to its own index, but since it normally requires a network round trip, the code table is usually the slower option. With an enum, I hesitated because Enum.valueOf() can have a worst-case time complexity of O(n), but implementing a lookup method backed by a hash map easily brings it down to about O(1).
// lookup method implemented with a hash map
enum Color {
RED("Red Color"),
GREEN("Green Color"),
BLUE("Blue Color");
private String description;
// static variable that holds the hash map
private static final Map<String, Color> map = new HashMap<>();
// initialize the hash map in a static block
static {
for (Color color : Color.values()) {
map.put(color.name(), color); // name as the key, Color as the value
}
}
// method that gets a value from the hash map
public static Color getByName(String name) {
return map.get(name); // O(1) lookup in the hash map
}
}
However, JPA's default implementation uses Enum.valueOf(), so a lookup through JPA still has O(n) time complexity. It is unnecessary in most cases, but when the enum constants are vast, implementing an AttributeConverter declared with @Converter(autoApply = true) reduces the time complexity of enum mapping in JPA/Hibernate.
Negotiating with enum Naming Rules
Enum constants have naming rules. So I wondered what to do when a name starts with a digit or contains special characters. Could something like v/v% actually be named? A simple way is to prefix names that start with a digit with _, and replace / with PER and % with PERCENT. The original string, before replacement, can be kept as a field.
// replacing special characters
enum Unit {
VOLUME_PER_VOLUME_PERCENT("v/v%"),
_1_PER_100_MM("1/100mm")
;
// constructor omitted.. about 500 constants
}
You may wonder whether something as complicated as units really needs to be an enum like this, but the real value of this approach is that the enum itself works as a class, which makes mathematical work such as unit conversion easy. And it can hold all kinds of domain logic.
That said, for codes with overly long strings or complicated special characters, this approach clearly has its limits. For example, when there is no short unique code and the value is long, such as " Dehydrated acoscobic acid, vitamin C* isotopic acid&*@ ", replacing the special characters to make an enum actually hurts readability. In such cases, unless domain logic is specifically required, it is more efficient to relate to a code table by primary key.
Changeability
Another concern is when a code needs to change. If we manage the codes in a code table and keep a separate primary key for each code, the change is limited to a bulk update in the database at most. Suppose we are using the enum codes a, b, c. If, after a while of storing the constant's .name with JPA's @Enumerated(EnumType.STRING) so that everything in the database is stored as a, b, c, the codes have to be changed to A, B, C, a mismatch between the database and the application arises immediately. The options in this situation are as follows.
- Bulk-update the database according to the changed enum definition
- Setting up a SQL script that runs automatically when Spring starts minimizes the gap between when the code changes and when the database changes. However, the risk of a mismatch from a missed script always remains.
- Work around it by modifying or redefining the enum attribute converter in the application so that a, b, c in the database map to the enums A, B, C
- The dependency exists only at the code level. However, when collaborating, problems can arise if someone who doesn't know the context misses redefining the converter.
- Additionally, redefine the enum attribute converter and, when matching fails, handle it with a predefined default constant (e.g. UNMATCHED)
- The problem is worked around for now and the system can keep running, but unless the existing use cases only read, or if there is domain logic that depends on the enum constant, it can lead to a system failure. It depends on the situation, but as long as it is tested well, the first option, the database script alone, is enough to get past this.
Solving Circular References Inside an enum Class
When you put domain logic into an enum, there are times when it has to reference another enum. Referencing something defined in a different enum class is no problem, but referencing within the same enum class causes a circular reference problem. For example, the following code does not compile.
enum Rank{
DIRECTOR(Rank.MANAGER), //Cannot refer to enum constant 'MANAGER' before its definition
MANAGER(Rank.ASSISTANT_MANAGER),
ASSISTANT_MANAGER(Rank.STAFF),
STAFF(null);
private final Rank lowerRank;
}
There are a few ways to solve this.
- Use a Supplier
- but it is constrained by declaration order
enum Rank{
STAFF(()-> null),
ASSISTANT_MANAGER(()-> Rank.STAFF),// can be referenced after STAFF is defined
MANAGER(()->Rank.ASSISTANT_MANAGER),
DIRECTOR(()-> Rank.MANAGER),
;
private final Supplier<Rank> lowerRank;
}
- Use a static block
- not affected by order, but the assignment is separated from the declaration
enum Rank{
DIRECTOR,
MANAGER,
ASSISTANT_MANAGER,
STAFF;
private Rank lowerRank; // assigned later, so final must be removed
static {
DIRECTOR.lowerRank = MANAGER;
MANAGER.lowerRank = ASSISTANT_MANAGER;
ASSISTANT_MANAGER.lowerRank = STAFF;
STAFF.lowerRank = null;
}
}
A static block is normally invoked when the class is loaded and runs before any constructor, but in an enum class the constant declarations are executed first, so initialization is possible after the enum objects have been created.
When a Large Number of enum Codes Must Be Written from an External Source
When dealing with a handful of domain constants, a developer can just type them in. But if the domain has 100 categories of constants with 100 constants each, that is ten thousand constants to type. And what if they keep changing? Not many people would type and maintain that. Manual work is also prone to human error.
A class is basically a text file with the .java extension, so if the format of the external source is consistent, this can be automated.
First prepare the external source (html, xlsx), then read it and turn it into an enum class as a .java file. If the output path overwrites the enum directory you are working in locally, anything odd in the generated result will raise a compile error, so checking whether it ran correctly is easy too.
public class EnumGenerator { // code written by ChatGPT
public static void main(String[] args) {
String excelFilePath = "codes.xlsx"; // path to the Excel file
String enumFileName = "ColorEnum.java"; // name of the .java file to generate
String packageName = "com.example.enums"; // package name
try {
// read the Excel file
FileInputStream file = new FileInputStream(new File(excelFilePath));
Workbook workbook = new XSSFWorkbook(file);
Sheet sheet = workbook.getSheetAt(0); // use the first sheet
// build the .java file content with a StringBuilder
StringBuilder enumContent = new StringBuilder();
enumContent.append("package ").append(packageName).append(";\n\n");
enumContent.append("public enum ColorEnum {\n");
// the first row is the header, so start reading from the second row
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row != null) {
Cell codeCell = row.getCell(0);
Cell descCell = row.getCell(1);
if (codeCell != null && descCell != null) {
String code = codeCell.getStringCellValue();
String description = descCell.getStringCellValue();
// add an enum entry
enumContent.append(" ")
.append(code.toUpperCase())
.append("(\"")
.append(description)
.append("\"),\n");
}
}
}
// remove the comma after the last enum entry and add a semicolon
int lastComma = enumContent.lastIndexOf(",");
if (lastComma != -1) {
enumContent.replace(lastComma, lastComma + 1, ";");
}
// add the constructor and field
enumContent.append("\n private final String description;\n\n")
.append(" ColorEnum(String description) {\n")
.append(" this.description = description;\n")
.append(" }\n\n")
.append(" public String getDescription() {\n")
.append(" return description;\n")
.append(" }\n")
.append("}");
// save to a file
Files.write(Paths.get(enumFileName), enumContent.toString().getBytes());
// close resources
workbook.close();
file.close();
System.out.println(enumFileName + " has been generated.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
An enum Is, After All, a Class
Because it is a class, renaming an enum requires building and deploying again. A code table can be replaced during operation by modifying the database.
Comments 0
Be the first to comment.