When your aggregate definition changes, you want that old stored aggregates are migrated to the new definition. If you can bring your server down, you could migrate them all at once. If this is no option, then you can write code to lazily migrate an aggregate as soon as it is loaded into the profile by the application. This article gives you examples on how to do this. The full specification can be found here: Conversion API.

Step-by-step guide

These scenarios are discussed:


Removing an entity

In this sample, an entity is removed from the aggregate definition in the target version. The implementation when an entity is removed from the metamodel is similar.

public class ConverterFromV1ToV2 implements IAggregateConverter {

	@Override
	public void preProcess(IAggregateConversionContext ctx) {}
	
	@Override
	public void postProcess(IAggregateConversionContext ctx) {}
	
	@Override
	public boolean addEntity(IEntityConversionContext ctx) {
		return "MyEntity".equals(ctx.getEntityName())
	}
	
	@Override
	public boolean setAttribute(IAttributeConversionContext ctx) {
		return "MyEntity".equals(ctx.getEntityName())
	}
	
	@Override
	public boolean setRelation(IRelationConversionContext ctx) {
		return "MyEntity".equals(ctx.getEntityName())
	}	
	
}

Renaming an entity

In this scenario, an entity is renamed in the target version.

public class ConverterFromV1ToV2 extends AggregateConverterAdapter {
	@Override
	public boolean addEntity(IEntityConversionContext ctx) {
		if ("OldEntity".equals(ctx.getEntityName())) {
			IEntityInstance instance = ctx.getProfile().createInstance("NewEntity");
			ctx.setCurrentInstance(instance);
			return true;
		}
		
		return false;
	}
}

Merging attribute values

In this scenario, the attributes FirstName and LastName in the source version have been removed in the target version and replaced with a single FullName attribute. The assumption is made that FirstName and LastName could not have been unknown and thus both FirstName and LastName were always saved with every Person instance. If this assumption could not be made, then the firstName and lastName attributes could be reset to null in addEntity to ensure that the FirstName and LastName attributes of different Person instances are not mixed together.

public class ConverterFromV1ToV2 extends AggregateConverterAdapter {
	private String firstName;
	private String lastName;
	
	@Override
	public boolean setAttribute(IAttributeConversionContext ctx) {
        boolean result = false;
		if ("Person".equals(ctx.getEntityName()) {
			if ("FirstName".equals(ctx.getAttributeName())) {
				firstName = ctx.getAttributeValue();
                result = true;
			} else if ("LastName".equals(ctx.getAttributeName())) {
				lastName = ctx.getAttributeValue();
                result = true;
			}
			
			if (firstName != null && lastName != null) {
				ctx.getCurrentInstance().setValue("FullName", String.format("%s %s", firstName, lastName));
				firstName = null;
				lastName = null;
			}
		}
 
        return result;
	}
}

Changing the type of an attribute

In this scenario, the attribute Person.Adult had its type changed from String to Boolean. We're assuming the attribute used to have values which are not convertible to Boolean using implicit conversion (eg. "adult", "minor")

public class ConverterFromV1ToV2 extends AggregateConverterAdapter {
	@Override
	public void setAttribute(IAttributeConversionContext ctx) {
		if ("Person".equals(ctx.getEntityName()) && "Adult".equals(ctx.getAttributeName())) {
			String value = ctx.getAttributeValue();
			if ("adult".equalsIgnoreCase(value)) {
				ctx.getCurrentInstance().setValue("Adult", BooleanValue.TRUE);
			} else {
				ctx.getCurrentInstance().setValue("Adult", BooleanValue.FALSE);
			}
            return true;
		}
 
        return false;
	}
}

Making a multi-valued attribute single-valued

In this scenario, the multiplicity of the Person.Hobbies attribute changed from multi-valued in the source version to single-valued in the target version. We would like to concatenate the saved values into a single, comma-separated string.

public class ConverterFromV1ToV2 extends AggregateConverterAdapter {
	private String currentEntity;
	private Map<GUID, StringBuilder> hobbies;
	@Override
	public void setAttribute(IAttributeConversionContext ctx) {
		if ("Person".equals(ctx.getEntityName()) && "Hobbies".equals(ctx.getAttributeName())) {
			if (!hobbies.containsKey(ctx.getInstanceId())) {
				hobbies.put(ctx.getInstanceId(), new StringBuilder(ctx.getAttributeValue()));
			} else {
				hobbies.get(ctx.getInstanceId()).append(", ").append(ctx.getAttributeValue());
			}
            return true;
		}
 
        return false;
	}
	
	@Override
	public void postProcess(IAggregateConversionContext ctx) {
		for (GUID instanceId : hobbies.keySet()) {
			if (ctx.hasInstance(instanceId)) {
				ctx.getInstance(instanceId).setValue("Hobbies", hobbies.get(instanceid).toString());
			}
		}
	}
}

Removing a relation and prevent those instances to load

In this scenario, the Person.Address relation was removed from the metamodel (but not the Address entity itself). Apart from being able to import saved Person instance which now no longer have the Address relation, we would also like to not import Address instances which were related to persons (while importing Address instances which are related to other entities). The easiest way is to remember which Address instances were related to persons and delete them from the profile in postProcess.

public class ConverterFromV1ToV2 extends AggregateConverterAdapter {
	
	private List<GUID> personAddresses;
	@Override
	public boolean setRelation(IRelationConversionContext ctx) {
		if ("Person".equals(ctx.getEntityName()) && "Address".equals(ctx.getRelationName())) {
			// store the instanceId of the target Address instance
			personAdresses.add(ctx.getRelationValue());
			
			// ignore this relation
			return true;
		}
		
		return false;
	}
	
	@Override
	public void postProcess(IAggregateConversionContext ctx) {
		// clean up Adress instances which used to be related to persons
		for (GUID instanceId : personAddresses) {
			if (ctx.hasInstance(instanceId)) {
				IEntityInstance address = ctx.getInstance(instanceId);
				ctx.getProfile().deleteInstance(adress, true);
			}
		}
	}
}

Migrating from a relation to an attribute

In this more complex scenario, the relation Person.Address of type Address(Street:String, StreetNumber:Integer) changed to attribute Person.Address:String. Other entities have relations to Address too, which must remain unchanged.

public class ConverterFromV1ToV2 extends AggregateConverterAdapter {
	private static class AttributeHolder extends HashMap<String, String> {}
	private Map<GUID, AttributeHolder> addresses;
	private Map<GUID, IEntityInstance> persons;
	private Map<GUID, IEntityInstance> others;
	@Override
	public boolean addEntity(IEntityConversionContext ctx) {
		// we will handle the import of Address instances ourselves
		if ("Address".equals(ctx.getEntityName())) {
			addresses.put(ctx.getInstanceId(), new AttributeHolder());
			return true;
		}
		// let default import proceed for other instances
		return false;
	}
	
	@Override
	public boolean setAttribute(IAttributeConversionContext ctx) {
		// save attributes of Address instances for later
		if ("Address".equals(ctx.getEntityName())) {
			adresses.get(ctx.getInstanceId()).put(ctx.getAttributeName(), ctx.getAttributeValue());
			return true;
		}
		// let default import proceed for other instances
		return false;
	}
	
	@Override
	public boolean setRelation(IRelationConversionContext ctx) {
		if ("Person".equals(ctx.getEntityName()) && "Address".equals(ctx.getRelationName())) {
			// save the relation of the current Person instance to an Address instance for later
			persons.put(ctx.getRelationValue(), ctx.getCurrentInstance());
			
			// we will handle the importing of this relation ourselves
			return true;
		} else if ("Address".equals(ctx.getRelationName())) {
			// save the relation of the current instance (other than Person) to an Address instance for later
			others.put(ctx.getRelationValue(), ctx.getCurrentInstance());
			
			// we will handle the importing ot his relation ourselves
			return true;
		}
		
		// let default import proceed for other relations
		return false;
	}
	
	@Override
	public void postProcess(IAggregateConversionContext ctx) {
		// now all information is known, transform Person.Address relation to Person.Address attribute
		// and also import other relations to Address instances that must remain unchanged
		
		// for all Adresses that were related to Persons
		for (GUID instanceId : persons.keySet()) {
			AttributeHolder address = adresses.get(instanceId);
			IEntityInstance person = persons.get(instanceId);
			
			person.setValue("Address", address.get("Street") + " " + address.get("StreetNumber"));
		}
		
		// for all other Addresses, import them and set the relations to them
		for (GUID instanceId : others.keySet()) {
			AttributeHolder holder = addresses.get(instanceId);
			IEntityInstance address = ctx.getProfile().createInstance("Address");
			IEntityInstance source = others.get(instanceId);
			
			address.setValue("Street", holder.get("Street"));
			address.setValue("StreetNumber", holder.get("StreetNumber"));
			
			source.setValue("Address", address.getInstanceReference());
		}
	}
}

Migrating an attribute to a relation

In this scenario, the attribute Address:String changed to relation Address:Address(Street, Number).

public class ConverterFromV1ToV2 extends AggregateConverterAdapter {
	@Override
	public boolean setAttribute(IAttributeConversionContext ctx) {
		if ("Person".equals(ctx.getEntityName()) && "Address".equals(ctx.getAttributeName())) {
			String value = ctx.getAttributeValue();
			int index = findStreetNumber(value);
			IEntityInstance address = ctx.getProfile().createInstance("Address");
			address.setValue("Street", value.substring(0, index));
			address.setValue("StreetNumber", value.substring(index+1));
			ctx.getCurrentInstance().setValue("Address", address.getInstanceReference());
            return true;
		}
 
        return false;
	}
	private int findStreetNumber(String value) {
		// parse old Address attribute value
	}
}