Multi-tier Migration

compared with
Version 19 by Robin Shen
on Feb 11, 2010 13:48.


Key
These lines were removed. This word was removed.
These lines were added. This word was added.

View page history


There are 1 changes. View first change.

 h2. Migrate super class and sub class separately
  
 Continue with version "1" of the Task class defined in [getting started] chapter:
 {code}
 package com.pmease.commons.xmt.bean;
  
 import java.util.Stack;
 import org.dom4j.Element;
 import com.pmease.commons.xmt.VersionedDocument;
  
 public class Task {
  enum Priority {HIGH, MEDIUM, LOW}
  
  public Priority priority;
  
  @SuppressWarnings("unused")
  private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
  Element element = dom.getRootElement().element("prioritized");
  element.setName("priority");
  if (element.getText().equals("true"))
  element.setText("HIGH");
  else
  element.setText("LOW");
  }
 }
 {code}
 Now we have class _CompileTask_ subclassing from Task class as below:
 {code}
 package example;
  
 import java.util.List;
  
 public class CompileTask extends Task {
  public List<String> srcFiles;
 }
 {code}
 Instance of _CompileTask_ can be serialized to XML with XMT as below:
 {code}
 package example;
  
 import java.util.ArrayList;
  
 import com.pmease.commons.xmt.VersionedDocument;
  
 public class Test {
  public static void main(String args[]) {
  CompileTask task = new CompileTask();
  task.priority = Task.Priority.HIGH;
  task.srcFiles = new ArrayList<String>();
  task.srcFiles.add("Class1.java");
  task.srcFiles.add("Class2.java");
  String xml = VersionedDocument.fromBean(task).toXML();
  saveXMLToFileOrDatabase(xml);
  }
  
  private static void saveXMLToFileOrDatabase(String xml) {
  // save XML to file or database here
  }
 }
 {code}
 The resulting XML will be:
 {code}
 <example.CompileTask version="1.0">
  <priority>HIGH</priority>
  <srcFiles>
  <string>Class1.java</string>
  <string>Class2.java</string>
  </srcFiles>
 </example.CompileTask>
 {code}
 Pay attention to version attribute of the root element: XMT examines the class hierarchy (except for class _java.lang.Object_) to get current version of each class, and concatenates them with period. Since class _Task_ is of version "1" and class _CompileTask_ is of version "0", the resulting version of the hierarchy (or composite version) is "1.0".
 When deserializing the compile task object from XML, XMT splits this composite version to get XML version for each class in the hierarchy, and repeats the process described in [getting started] chapter for each of these classes. So if class _Task_ is evolved to use numeric priority field, we simply add migrate methods in _Task_ class, while keep _CompileTask_ class intacted. On the other hand, if we evolve class _CompileTask_ to include a _destDir_ field, we can define the migrate method in _CompileTask_ like below, while keep class _Task_ intacted:
 {code}
 package example;
  
 import java.util.List;
 import java.util.Stack;
  
 public class CompileTask extends Task {
  public List<String> srcFiles;
  
  public String destDir = "classes";
  
  @SuppressWarnings("unused")
  private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
  dom.getRootElement().addElement("destDir").setText("classes");
  }
 }
 {code}
 This separation of concerns is very important since there might exist many sub classes, and you certainly do not want to modify those sub classes if super class is evolved, and vice versa.
  
 h2. Address class hierarchy change problem
  
 Now we introduce class _AbstractCompileTask_ in the middle of _Task_ and _CompileTask_ like below:
 {code}
 package example;
  
 public abstract class AbstractCompileTask extends Task {
  public String options = "-debug";
 }
 {code}
 {code}
 package example;
  
 import java.util.List;
 import java.util.Stack;
  
 public class CompileTask extends AbstractCompileTask {
  public List<String> srcFiles;
  
  public String destDir = "classes";
  
  @SuppressWarnings("unused")
  private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
  dom.getRootElement().addElement("destDir").setText("classes");
  }
 }
 {code}
 When deserialize from aforementioned "1.0" XML, XMT does the following:
 # Split "1.0" into a versions stack containing element "0" and "1" from top to bottom.
 # Set current class to be _CompileTask_.
 # Pop up top element "0" from versions stack, compare it with current version of current class and invoke migration methods as necessary. This results in invocation of method _migrate1_ of class _CompileTask_, with current versions stack passed as param _versions_.
 # Set current class to super class of _CompileTask_, which is now _AbstractCompileTask_.
 # Pop up top element "1" from versions stack, and compare it with current version of current class. There is a class mis-match at this point: version "1" recorded in XML is derived from class _Task_, while current class is _AbstractCompileTask_.
  
 This issue can be solved by introducing another migrate method into class _CompileTask_:
 {code}
 package example;
  
 import java.util.List;
 import java.util.Stack;
  
 public class CompileTask extends AbstractCompileTask {
  public List<String> srcFiles;
  
  public String destDir = "classes";
  
  @SuppressWarnings("unused")
  private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
  dom.getRootElement().addElement("destDir").setText("classes");
  }
  
  @SuppressWarnings("unused")
  private void migrate2(VersionedDocument dom, Stack<Integer> versions) {
  versions.push(0);
  dom.addElement("options").setText("-debug");
  }
 }
 {code}
 This new migrate method pushes version "0" (current version of class _AbstractCompileTask_) into versions stack. The stack now contains element "0" and "1" from top to bottom, which can be used to handle data migration of class _AbstractCompileTask_ and _Task_ correctly. It also adds "options" element so that compile task objects deserialized from XML of old versions has the default compile options value of "-debug".
  
 Now that we've successfully handled the case of adding new class into the hierarchy, but how about removing an existing class? Let's assume that class _Task_ is removed and class _AbstractCompileTask_ needs to take care of the priority field. In order to deserialize from XML of old versions, we write _AbstractCompileTask_ as below (assume class _Task_ is at version "2" when it is removed):
 {code}
 package example;
  
 import java.util.Stack;
  import org.dom4j.Element;
 import com.pmease.commons.xmt.VersionedDocument;
 import com.pmease.commons.xmt.MigrationHelper;
  
 public class AbstractCompileTask {
  public int priority;
  
  public String options = "-debug";
  
  @SuppressWarnings("unused")
  private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
  int taskVersion = versions.pop();
  MigrationHelper.migrate(String.valueOf(taskVersion), TaskMigrator.class, dom);
  }
  
  private static class TaskMigrator {
  
  @SuppressWarnings("unused")
  private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
  Element element = dom.getRootElement().element("prioritized");
  element.setName("priority");
  if (element.getText().equals("true"))
  element.setText("HIGH");
  else
  element.setText("LOW");
  }
  
  @SuppressWarnings("unused")
  private void migrate2(VersionedDocument dom, Stack<Integer> versions) {
  Element element = dom.getRootElement().element("priority");
  if (element.getText().equals("HIGH"))
  element.setText("10");
  else if (element.getText().equals("MEDIUM"))
  element.setText("5");
  else
  element.setText("1");
  }
  }
 }
 {code}
 The newly added migrate method pop up the top element from versions stack to get the version intended to migrate class _Task_, and call _MigrationHelper.migrate_ to apply migration methods defined in class _TaskMigrator_ as necessary. Migration methods defined in class _TaskMigrator_ are simply copied from the deleted _Task_ class.
  
 When migrate from aforementioned "1.0" XML with this new class hierarchy, XMT does the following:
 # Split "1.0" into a versions stack containing element "0" and "1" from top to bottom.
 # Set current class to _CompileTask_.
 # Pop up top element "0" from versions stack, compare it with current version of current class and invoke migration methods as necessary. This results in invocation of method _migrate1_ and _migrate2_ defined in class _CompileTask_:
 ## Method _migrate1_ is invoked to add "destDir" element with value "classes", and the versions stack contains element "1" after invocation.
 ## Method _migrate2_ is invoked to add "options" element with value "-debug", and the versions stack contains element "0" and "1" from top to bottom.
 # Set current class to super class of _CompileTask_, which is now _AbstractCompileTask_.
 # Pop up top element "0" from versions stack, compare it with current version of current class and invoke migration methods as necessary. As a result of this, XMT invokes method _migrate1_ of class _AbstractCompileTask_, with current versions stack (now contains only one element "1") passed as param _versions_. This method pops up top element from versions stack, which is "1", and pass it as param _fromVersion_ when invoking method _MigrationHelper.migrate_. This results in invocation of method _migrate2_ of class _TaskMigrator_ to migrate enum based priority to be numeric based.
 # Now the versions stack is empty and the migration process is done successfully.