Aaron Roller Aaron Roller - 2 months ago 23
Java Question

Backwards Compatibility of Enum Additions for Objectify/Appengine

We are using Objectify with Google App Engine for Java. We are persisting a variety of enum constants in the datastore using the supplied EnumTranslatorFactory which simply stores/loads the constant using the Enum#name(). This works well.

When we release new versions of our app to GAE, the new version lives next to the old version(s) both serving requests simultaneously to clients. This is explained well by Google's traffic splitting docs.

Upgrades to the system introduce new Enum constants which cause errors during loading. For example:

Version 1 has the following enum:

enum Meal{BREAKFAST,LUNCH,DINNER}


Version 2 has the additional constant added to the enum to support British meals:

enum Meal{BREAKFAST,LUNCH,TEA,DINNER}


While testing version 2 of the app, TEA will be persisted with some Entity. Subsequently Version 1 will load that Entity, Objectify will attempt to convert TEA into a Enum using Enum#valueOf(...) which throws a runtime exception.

Objectify docs explain Data Migration for Enums, but it doesn't satisfy the above situation.

I'm interested in suggestions about how to best handle this situation.

Answer

First, provide an Interface that will provide a default value if the enum is not known.

public interface EnumWithDefault<E extends Enum<E>> {
    E getDefault();
}

An Enum that may have future additions should implement this Interface:

public enum MyEnum implements EnumWithDefault<MyEnum>{
  ENUM_IN_VERSION_1, FUTURE;

  public MyEnum getDefault(){ return FUTURE; }
}

Register a TranslatorFactory that will provide default if implemented:

       return new ValueTranslator<Enum<?>, String>(path, String.class) {
    @Override
    public Enum<?> loadValue(String value, LoadContext ctx) {
        try{
           return Enum.valueOf((Class<Enum>)type, value.toString());
        }catch(Exception e){
           if (EnumWithDefault.class.isAssignableFrom(enumType)) {
                EnumWithDefault<E> any = (EnumWithDefault<E>) enumType.getEnumConstants()[0];
                result = any.getDefault();
           }else{
              throw e;
           } 
        }
    }

Version 2 deployed with new Enum:

public enum MyEnum implements EnumWithDefault<MyEnum>{
  ENUM_IN_VERSION_1, ENUM_IN_VERSION_2, FUTURE;

  public MyEnum getDefault(){ return FUTURE; }
}

When Version 2 of the app is deployed and ENUM_IN_VERSION_2 is stored in the datastore related to some Entity, the response differs when hitting the endpoints of the two versions.

Hitting the first version returns the value FUTURE allowing the client to present an appropriate message:

http://1.myapi.appspot.com/entities

returns:

<myEntity id='xyz' category='FUTURE' />

Hitting version 2 provides the new enum:

http://2.myapi.appspot.com/entities

returns:

<myEntity id='xyz' category='ENUM_IN_VERSION_2' />

This solution allows additional enumerations to be added and used in a later release while older versions present a value to the client per the contract that "Future" is possible.

Comments