Annotation Interface OnSchemaChange


@Retention(RUNTIME) @Target({ANNOTATION_TYPE,METHOD}) @Documented public @interface OnSchemaChange
Annotation for methods that are to be invoked whenever an object's schema has just changed, in order to apply arbitrary "semantic" schema migration logic.

The annotated method is given access to all of the previous object version's fields, including fields that have been deleted or whose types have changed in the new schema. This allows the object to perform any schema migration "fixups" that may be required before the old information is lost for good.

Simple changes that only modify a simple field's type can often be handled automatically; see UpgradeConversionPolicy, @PermazenField.upgradeConversion(), and Encoding.convert() for details.

The annotated method must be an instance method (i.e., not static), return void, and take from one to three parameters. The first parameter must have type Map<String, Object> oldValues and will be an immutable map containing the values of all the fields in the previous schema version of the object indexed by field name. The optional second and third parameters have type SchemaId and identify the old and new schemas (respectively).

In many cases, the simplest way to handle schema changes is to use the presence or absence of fields in oldValues to determine what migration work needs to be done. For example:

      @OnSchemaChange
      private void applySchemaChanges(Map<String, Object> oldValues) {

          // At some point we added a new field "balance"
          if (!oldValues.containsKey("balance"))
              this.setBalance(this.calculateBalanceForSchemaMigration());

          // At some point we replaced "fullName" with "lastName" & "firstName"
          if (oldValues.containsKey("fullName")) {
              final String fullName = (String)oldValues.get("fullName");
              if (fullName != null) {
                  final int comma = fullName.indexOf(',');
                  this.setLastName(comma == -1 ? null : fullName.substring(0, comma));
                  this.setFirstName(fullName.substring(comma + 1).trim());
              }
          }
          // ...etc
      }
 

Note: PermazenCounterField values are represented in oldValues as Longs.

A class may have multiple @OnSchemaChange-annotated methods.

Incompatible Object Type Changes

Permazen supports arbitrary Java model schema changes across schemas, including adding and removing Java types. This creates a few caveats relating to schema migration.

First, if an object's type no longer exists in the new schema, migration is not possible, and any attempt to do so will throw a TypeNotInSchemaException. Such objects are still accessible however (see UntypedPermazenObject).

Secondly, it's possible for an old field to have a value that simply doesn't exist in the new schema. When this happens, it's not possible to provide the old value to an @OnSchemaChange method in its original form. This can happen in two ways:

  • A reference field refers to an object whose type no longer exists in the new schema; or
  • An Enum field refers to an Enum type that no longer exists, or whose identifiers have changed (this is really just a special case of the previous scenario: when an Enum type's constants change in any way, the new Enum is treated as a completely new type).

Therefore, the following special rules apply to the oldValues map:

  • For a reference field whose type no longer exists, the referenced object will appear as an UntypedPermazenObject.
  • For Enum fields, old values are always represented as EnumValue objects. For consistency's sake, this is true even if the associated field's type has not changed.

Meta-Annotations

This annotation may be configured indirectly as a Spring meta-annotation when spring-core is on the classpath.

See Also: