Better way to store Enum values in MongoDB
Tags: javascript, java, mongodb, enum
Published: 2020-09-26 21:41:27



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

```java
public enum ProcessType {
	CONVERT_ONE_TO_ONE,
	CONVERT_ONE_TO_MANY;
}
```
and it is registered with mongo codec provider as 

```java
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](https://mongodb.github.io/mongo-java-driver/3.5/bson/pojos/#enum-support) 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.

```javascript
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](https://github.com/typestack/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.

```java
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.

```java
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

```java
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`.

```java
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

```java
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](https://www.ps2pdf.com/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

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

and on Back-end 
```java
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.
```java
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.