Storing Enums as Strings in Mongodb

Storing Enums as strings in MongoDB

The MongoDB .NET Driver lets you apply conventions to determine how certain mappings between CLR types and database elements are handled.

If you want this to apply to all your enums, you only have to set up conventions once per AppDomain (usually when starting your application), as opposed to adding attributes to all your types or manually map every type:

// Set up MongoDB conventions
var pack = new ConventionPack
{
new EnumRepresentationConvention(BsonType.String)
};

ConventionRegistry.Register("EnumStringConvention", pack, t => true);

C# - Loading data (enum stored as string) from MongoDB throw exception if cannot be mapped back to enum, for example old enum value has been deleted

First you need to define the unknown enum as the first enum. I myself prefer storing them as explicit ints to prevent renaming of enums corrupt the database, but that is not nessecary in your case.

enum CarType {
Unknown
SportCar,
Suv,
Hatchback
}

Then you need to write and register a custom deserializer for your enum in order to implement the fallback to "unknown" as this. It will try to parse your database value to the enum and fallbacks to default (Unknown) if unable to parse it.

 public class CustomEnumSerializer<TEnum> : MongoDB.Bson.Serialization.Serializers.EnumSerializer<TEnum>
where TEnum : struct
{
public override TEnum Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var type = context.Reader.GetCurrentBsonType();
string val;

switch (type)
{
case BsonType.String:
val = context.Reader.ReadString() ?? "";
break;
case BsonType.Int32:
val = context.Reader.ReadInt32().ToString();
break;
case BsonType.Int64:
val = context.Reader.ReadInt64().ToString();
break;
case BsonType.Null:
return default(TEnum);
default:
return base.Deserialize(context, args);
}

if (Enum.TryParse(val, true, out TEnum result) && Enum.IsDefined(typeof(TEnum), result))
{
return result;
}
return default(TEnum);
}
}

Register the custom serializer as:

BsonClassMap.RegisterClassMap<ClassThatHoldsTheProperty>(ms =>
{
ms.AutoMap();
ms.GetMemberMap(i => i.CarTypeProperty)
.SetSerializer(new CustomEnumSerializer<CarType>());
});

Changing an enum value stored as a string inside a document in MongoDB

once you decorate an enum property with [BsonRepresentation(BsonType.String)] you don't need to do any casts in your code. the attribute instructs the driver to store the stirng name of the enum value in the db and when deserializing it, set it back to the appropriate enum value. so your application code can just deal with enums.

    var statusValue = Status.Good;

var filter = Builders<Document>.Filter.Eq(d => d.Status, Status.Bad);
var update = Builders<Document>.Update.Set(d => d.Status, statusValue);

collection.UpdateMany(filter, update);

test program:

using MongoDB.Bson;using MongoDB.Bson.Serialization.Attributes;using MongoDB.Entities;using MongoDB.Entities.Core;
namespace StackOverFlow{ public enum Status { Good, Bad }
public class Document : Entity { [BsonRepresentation(BsonType.String)] public Status Status { get; set; } }
public static class Program { private static void Main() { new DB("test");
new Document { Status = Status.Bad }.Save();
var statusValue = Status.Good;
DB.Update<Document>() .Match(f => f.Eq(d => d.Status, Status.Bad)) .Modify(b => b.Set(d => d.Status, statusValue)) .Execute(); } }}

Storing enum to MongoDb (for managing tag names)

This is typical use case of referencing other collections. So, you should have 2 collections:

Authors collection:

{
_id: ObjectId,
name: String,
... // Other fields
}

Books collection:

{
_id: ObjectId,
authors: [ ObjectId ], // References to documents from Author collection
... // Other fields
}

So, in authors property of the Books collection, you store _id values of all the authors. Then when you fetch book document, you can easily fetch up-to-date authors data from Authors collection.

Store enum MongoDB

TL;DR: Strings are probably the safer choice, and the performance difference should be negligible. Integers make sense for huge collections where the enum must be indexed. YMMV.

I have thought of storing it using integers which I would assume uses less space than storing strings for everything that could easily be expressed as an integer

True.

other upside I see of using integers is that if I wanted to rename an achievement or rank I could easily change it without even having to touch the database.

This is a key benefit of integers in my opinion. However, it also requires you to make sure the associated values of the enum don't change. If you screw that up, you'll almost certainly wreak havoc, which is a huge disadvantage.

A benefit I see for using strings is that the data requires less processing before it is used

If you're actually using an enum data type, it's probably some kind of integer internally, so the integer should require less processing. Either way, that overhead should be negligible.

Is there an strong reason to use either integers or strings?

I'm repeating a lot of what's been said, but maybe that helps other readers. Summing up:

  • Mixing up the enum value map wreaks havoc. Imagine your Declined states are suddenly interpreted as Accepted, because Declined had the value '2' and now it's Accepted because you reordered the enum and forgot to assign values manually... (shudders)
  • Strings are more expressive
  • Integers take less space. Disk space doesn't matter, usually, but index space will eat RAM which is expensive.
  • Integer updates don't resize the object. Strings, if their lengths vary greatly, might require a reallocation. String padding and padding factor should alleviate this, though.
  • Integers can be flags (not yet queryable (yet), unfortunately, see SERVER-3518)
  • Integers can be queried by $gt / $lt so you can efficiently implement complex $or queries, though that is a rather arcane requirement and there's nothing wrong with $or queries...

Saving enum into mongoDB

I assume you mean saving an enum value into a collection.

Basically, you just add it into your entity model, like so:

@Document(collection = "MyEntity ")
public class MyEntity {
public SnapshotType snapshotType;
}

It will store it as a string in mongo, and automagically convert when you read it out.

Spring -Mongodb storing/retrieving enums as int not string

After a long digging in the spring-mongodb converter code,
Ok i finished and now it's working :) here it is (if there is simpler solution i will be happy see as well, this is what i've done ) :

first define :

public interface IntEnumConvertable {
public int getValue();
}

and a simple enum that implements it :

public enum tester implements IntEnumConvertable{   
vali(0),secondvali(1),thirdvali(5);

private final int val;
private tester(int num)
{
val = num;
}
public int getValue(){
return val;
}
}

Ok, now you will now need 2 converters , one is simple ,
the other is more complex. the simple one (this simple baby is also handling the simple convert and returns a string when cast is not possible, that is great if you want to have enum stored as strings and for enum that are numbers to be stored as integers) :

public class IntegerEnumConverters {
@WritingConverter
public static class EnumToIntegerConverter implements Converter<Enum<?>, Object> {
@Override
public Object convert(Enum<?> source) {
if(source instanceof IntEnumConvertable)
{
return ((IntEnumConvertable)(source)).getValue();
}
else
{
return source.name();
}
}
}
}

the more complex one , is actually a converter factory :

public class IntegerToEnumConverterFactory implements ConverterFactory<Integer, Enum> {
@Override
public <T extends Enum> Converter<Integer, T> getConverter(Class<T> targetType) {
Class<?> enumType = targetType;
while (enumType != null && !enumType.isEnum()) {
enumType = enumType.getSuperclass();
}
if (enumType == null) {
throw new IllegalArgumentException(
"The target type " + targetType.getName() + " does not refer to an enum");
}
return new IntegerToEnum(enumType);
}
@ReadingConverter
public static class IntegerToEnum<T extends Enum> implements Converter<Integer, Enum> {
private final Class<T> enumType;

public IntegerToEnum(Class<T> enumType) {
this.enumType = enumType;
}

@Override
public Enum convert(Integer source) {
for(T t : enumType.getEnumConstants()) {
if(t instanceof IntEnumConvertable)
{
if(((IntEnumConvertable)t).getValue() == source.intValue()) {
return t;
}
}
}
return null;
}
}
}

and now for the hack part , i personnaly didnt find any "programmitacly" way to register a converter factory within a mongoConverter , so i digged in the code and with a little casting , here it is (put this 2 babies functions in your @Configuration class)

      @Bean
public CustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
converters.add(new IntegerEnumConverters.EnumToIntegerConverter());
// this is a dummy registration , actually it's a work-around because
// spring-mongodb doesnt has the option to reg converter factory.
// so we reg the converter that our factory uses.
converters.add(new IntegerToEnumConverterFactory.IntegerToEnum(null));
return new CustomConversions(converters);
}

@Bean
public MappingMongoConverter mappingMongoConverter() throws Exception {
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(appContext);
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
MappingMongoConverter mongoConverter = new MappingMongoConverter(dbRefResolver, mappingContext);
mongoConverter.setCustomConversions(customConversions());
ConversionService convService = mongoConverter.getConversionService();
((GenericConversionService)convService).addConverterFactory(new IntegerToEnumConverterFactory());
mongoConverter.afterPropertiesSet();
return mongoConverter;
}

Enums in MongoDB

Storing enum values in MongoDB as strings is perfectly fine, and yes, if you index the field I'd expect the performance to be comparable to indexed integer queries. It's certainly more expressive than using integers.

The only real downside is that they'll take more space if your enum strings are somewhat long, but that's a pretty trivial concern.



Related Topics



Leave a reply



Submit