loading...
Cover image for Better way to store Enum values in MongoDB

Better way to store Enum values in MongoDB

harithay profile image harithay ・5 min read

If you want to save Java Enum value to DB as Enum name, then the Mongo driver supports that. For example, if you have an enum

public enum ProcessType {
    CONVERT_ONE_TO_ONE,
    CONVERT_ONE_TO_MANY;
}

and it is registered with mongo codec provider as

import org.bson.codecs.pojo.ClassModel;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.bson.codecs.pojo.PojoCodecProvider.Builder;
import com.ps2pdf.models.enums.ProcessType; // Local
...
Builder builder = <your mongo builder>
ClassModel<ProcessType> classModel = ClassModel.builder(ProcessType.class).build();
builder.register(classModel);

then, whenever you save an instance of a class with a property type ProcessType to DB, the resulting Mongo document will have string value CONVERT_ONE_TO_ONE or CONVERT_ONE_TO_MANY for that property.

if that is all you need, then the following is not for you. In that case, you can follow Mongo POJO tutorial to guide you.

Following is a way to store the value associated with a Java Enum in the MongoDB. Why would someone want to do that?

  • Java (also TypeScript) has a convention of using upper case name in Enums, which is probably inherited from the use of uppercase names for constants.
  • I prefer to assign lowercase values to Object properties (as many people do)
  • Prevent tying property name to its value. I prefer to keep the variable names short and the value assigned to it could be anything.

Above are a few reasons for saving Enum values instead of names to MongoDB.

Another pain point for me was comparing decoded Enum values in front-end. Following is the front-end TypeScript Enum for above Java Enum.

export enum WebsocketProcessType {
    CONVERT_ONE_TO_ONE = 'convert-one-to-one',
    CONVERT_ONE_TO_MANY = 'convert-one-to-many',
}

If we were to use the default Enum decoder provided by Mongo Java driver, then our values have to be the same as names on Java Enum, which is too coupled and strict for us to write better readable code.

With instruction bellow and the use of Class Transformer to decode data sent from backend, you will be able to seamlessly map Java classes to TypeScript(js) classes.

Implementation

Steps:

  1. Create and register a codec provider with Mongo Code Registry which Mongo uses to determine which Enum decoder to use a Java Enum value
  2. Create and register Enum decoder for ProcessType
  3. Create and register Enum with DB

I will make some classes as Generic since this can be used to decode all any Enum.

Create a codec provider

I will not provide imports as you should have Mongo Java Driver and with modern IDEs, you can auto-import all imports.

public class EnumCodecProvider implements CodecProvider {
    @Override
    public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
        if (clazz == ProcessType.class) {
            return (Codec<T>) new ProcessTypeCodec();
        } 
        return null; // Don't throw here, this tells Mongo this provider doesn't provide a decoder for the requested clazz
    }
}

This is pretty simple. Mongo decoder, call get method of provider to get a decoder for a Class that it doesn't know how to decode. When it calls ....get(ProcessType.class, MongoRegisty) we will return our ProcessTypeCodec, which knows how to decode a ProcessType Enum.

CodecRegistry pojoCodecRegistry = 
                fromRegistries(MongoClient.getDefaultCodecRegistry(),
                    CodecRegistries.fromRegistries(
                        CodecRegistries.fromProviders(new EnumCodecProvider())
                    ), 
                );
MongoClientOptions options = MongoClientOptions.builder().codecRegistry(pojoCodecRegistry).build();
// Register above option with the MongoClient

Above registers the EnumCodeProvider with the mongo registry.

Create Enum Codec to encode/decode our Enum

I made an abstract decoder to put all common code that required to decode our Enum to avoid code duplication

abstract class AbstractCodec<T extends Enum<T>> implements Codec<T> {
    public AbstractCodec() {
    }

    @Override
    final public void encode(final BsonWriter writer, final T value, final EncoderContext encoderContext) {
        String val = ((Enum) value).toString();
        writer.writeString(val);
    }

    @Override
    final public T decode(final BsonReader reader, final DecoderContext decoderContext) {
        try {
            String value = reader.readString();
            Method method = getEncoderClass().getDeclaredMethod("fromValue", String.class);
            T enumName = (T) method.invoke(null, value);
            return enumName;
        }catch(Exception e) {
            try {
                String value = reader.readString();
                Method method = getEncoderClass().getDeclaredMethod("getDefaultValue");
                T storageType = (T) method.invoke(null, value);
                return storageType;
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
        return null;
    }

    public abstract Class<T> getEncoderClass();
}

Note that we call toString on the encode method above. This toString method must be implemented on ProcessType Enum class to provide the value of the Enum name.

On decode method, we call fromValue and getDefaultValue on our ProcessType Enum to get Enum name associated with a particular value stored on DB. Yes, you have to use Java reflection to execute method on a object of a class type T. If you don't like to use reflection, you can push the decode class to the ProcessTypeCodec and directly call the static method (see Enum Implementation below).

To sum up, when the decoder gets a request with a string value, i.e. "convert-one-to-one", we get the class name associated with this codec and calls a static method fromValue to get the Enum name that corresponds to the string value.

Following is the ProcessTypeCodec.

public class ProcessTypeCodec extends AbstractCodec<ProcessType> {

    public ProcessTypeCodec() {
        super();
    }

    @Override
    public Class<ProcessType> getEncoderClass() {
        return ProcessType.class;
    }
}

This just let Mongo know the class which this Codec can encode/decode.

Implement and register ProcessType enum

public enum ProcessType {
    CONVERT_ONE_TO_ONE("convert-one-to-one"),
    CONVERT_ONE_TO_MANY("convert-one-to-many");

    private String value;
    private static final Map<String, ProcessType> ENUM_MAP;
    static {
        Map<String, ProcessType> map = new HashMap<String, ProcessType>();
        for (ProcessType instance : ProcessType.values()) {
            map.put(instance.value(), instance);
        }
        ENUM_MAP = Collections.unmodifiableMap(map);
    }
    ProcessType(String type) {
        this.value = type;
    }

    public String value() {
        return this.value;
    }

    public static ProcessType fromValue(String value) {
        return ENUM_MAP.get(value);
    }

    /**
     * Used by the Mongo codec
     * 
     * @return
     */
    public static ProcessType getDefaultValue() {
        return CONVERT_ONE_TO_ONE;
    }

    /**
     * Required to properly convert Java Enum name to value.
     * Value is used by front-end and usually uses <br>
     * 1. lowercase <br>
     * 2. dashes instead of underscores <br> <br>
     */
    @Override
    public String toString() {
        return this.value;
    }
}

ENUM_MAP is just to speed up the process. It allows us the decoder to convert a string to Enum name in O(1) time complexity. Default is your preference, I used here an Enum name but this is usually null.

See above for registering classes with the Mongo class registry.

Our PS2PDF Video Compressor takes in some augments as Enum to populate the command argument needed for FFMPEG to compress or convert video files. For example, we have a output extension Enum on front-end

export enum OutputExtension {
    MP4 = '.mp4',
    WEBM = '.webm'
}

and on Back-end

public enum OutputExtension {
    MP4(".mp4"),
    WEBM(".webm")
    // ... rest of the code similar to above ProcessType Enum
}

when we store command line argument generated from TypeScript to DB on a document, it stores the actual value that we want i.e. .mp4 extenstion on DB. On the back-end, our decoder maps that value to related Java Enum properly. When we want to use this to build the FFMPEG command we can actually use the Enum value directly.
i.e.

class Request { // Sample class that deals with request document stored in DB
    OutputExtension outoutExtenstion;
}

List<String> cmd = List.of("ffmpeg", ..., "-o", Request.outoutExtenstion);
// This generates % ffmpeg ... -o .mp4

Hope this helps you to write more readable code. If you find any errors in this document, please let me know to rectify them.

Posted on by:

harithay profile

harithay

@harithay

I am a Software Engineer. I developed a tool call PS2PDF.com that let people convert videos, PDFs and images.

Discussion

pic
Editor guide