First a bit of terminology.  The idea is that there is an "aggregate" in
the domain-driven design sense of the word, i.e. an object graph that can
be considered to be one composite "thing".  For example, an Employee object
might reference related Address records, and also reference another
Employee object in the role of Supervisor.  The Address records are part of
the Employee aggregate, the Supervisor is not - it's an independent object
that just happens to be referenced.  The acid test is whether it would make
sense to keep the record around if you deleted its parent. If you would
always delete the child at the same time as the parent, the child is part
of the parent's aggregate.

So, my code lets me stop serializing at the boundary of the aggregate -
i.e. I could serialize just one Employee with all it's "own" data, no
matter how many objects that data is spread across.  References to things
outside the aggregate are serialized as simple key values.  Then when
deserializing, you can look up the keys and wire everything back together
again.

This might be a more complex case than what you have in mind, but you can
think of your case as a reductive one where the aggregate is always one
table in size.

Here's the full code for serializing:

// This is the usual setup for serializing Hibernate stuff
final XStream xstream = new XStream() {
  protected MapperWrapper wrapMapper(final MapperWrapper next) {
    return new HibernateMapper(next);
  }
};
xstream.registerConverter(new HibernateProxyConverter());
xstream.registerConverter(new
HibernatePersistentCollectionConverter(xstream.getMapper()));
xstream.registerConverter(new
HibernatePersistentMapConverter(xstream.getMapper()));
xstream.registerConverter(new
HibernatePersistentSortedMapConverter(xstream.getMapper()));
xstream.registerConverter(new
HibernatePersistentSortedSetConverter(xstream.getMapper()));

// These are my custom converters
xstream.registerConverter(new UnitExternalRefExportConverter());
xstream.registerConverter(new UnitPruningConverter(xstream.getMapper(),
xstream.getReflectionProvider()));

// I usually leave out internal database bookkeeping and audit data
xstream.omitField(BaseDomainObject.class, "id");
xstream.omitField(BaseDomainObject.class, "createdDate");
xstream.omitField(BaseDomainObject.class, "modifiedDate");
xstream.omitField(BaseDomainObject.class, "modifiedBy");

return xstream.toXML(unit);

"Unit" is the aggregate I'm serializing.  I won't bother to explain its
structure.  BTW I'm still on the older version of XStream so I copied the
Hibernate bits from the doc.  I presume the rest of my approach still works
in the new version.

On to the interesting part, the custom converters.
UnitExternalRefExportConverter looks like this:

/*
 * Converts fields that are external to the unit aggregate root
 * and therefore should be represented by symbolic references
 * rather than included in toto
 */
public class UnitExternalRefExportConverter implements Converter {

 @Override
 public void marshal(Object obj, HierarchicalStreamWriter writer,
   MarshallingContext arg2) {
  BaseDomainObject bdo = (BaseDomainObject) obj;
  String textID = Utility.getTextIDValue(bdo,
Utility.getTextKeyField(bdo.getClass()));
  writer.setValue(textID == null ? "" : textID);
 }
 @Override
 public Object unmarshal(HierarchicalStreamReader reader,
   UnmarshallingContext context) {
  throw new Error ("UnitExternalRefExportConverter should not be used for
unmarshalling.  Use UnitExternalRefImportConverter instead");
 }
 @Override
 public boolean canConvert(Class clazz) {
  return (clazz.equals(Service.class) ||
    clazz.equals(UnitType.class) ||
    clazz.equals(Audience.class) ||
    clazz.equals(LearningTeam.class) ||
    clazz.equals(Leader.class) ||
    clazz.equals(ContentPartner.class) ||
    clazz.equals(User.class) ||
    clazz.equals(AssessmentStrategy.class) ||
    clazz.equals(Category.class) ||
    clazz.equals(PDCollection.class) ||
    clazz.equals(InteractionType.class) ||

    clazz.equals(QuestionnaireFormat.class) ||
    clazz.equals(QuestionnaireType.class) ||
    clazz.equals(QuestionType.class) ||
    clazz.equals(QuestionFormat.class) ||
    clazz.equals(Validation.class) ||
    clazz.equals(com.medeserv.mesquest.Service.class)
);

 }
}

The canConvert method just lists all of the classes that are external to
the aggregate and should NOT be serialized.  In your table by table
scenario, this would be simply any class that is not the class mapped to
the table you're serializing.

The marshal method just writes a simple key value into the output stream
(where the default converter would continue to recurse into the associated
object).  My code does some gymnastics to derive a database-independent
textual key value here, as I often want to import the xml back into a
different database, but within one database you could just use the foreign
key.  In terms of this code that would be obj.getId().

UnitPruningConverter is essentially a way to separate out user data from
configuration data so I can serialize just the "static" parts of the object
graph e.g. for copying a configuration from one server to another.

/*
 * Prunes out parts of the tree that should not be serialized - primarily
student data
 * We can't simply use @Transient to do this as that would then nobble
Hibernate
 * Default unmarshalling is fine for this case - we'll always just have
empty java collections
 */
public class UnitPruningConverter extends ReflectionConverter {

 public UnitPruningConverter(Mapper mapper,
   ReflectionProvider reflectionProvider) {
  super(mapper, reflectionProvider);
 }


 @Override
 protected void marshallField(MarshallingContext context, Object newObj,
   Field field) {
  if (field.getName().equals("collection")) {
   // Make sure these collections are always empty
   super.marshallField(context, "", field);
  }
  else {
   super.marshallField(context, newObj, field);
  }
 }

 @Override
 public boolean canConvert(Class clazz) {
  return (clazz.equals(UnitProgressManager.class) ||
    clazz.equals(Unit_UserUnitHistoryManager.class) ||
    clazz.equals(Unit_UserUnitAccessManager.class) ||
    clazz.equals(Unit_LearningProjectUnitManager.class) ||
    clazz.equals(Unit_UserNoteManager.class) ||
    clazz.equals(InteractionProgressManager.class) ||
    clazz.equals(InteractionAvailableManager.class) ||
    clazz.equals(UnitAlertStatusManager.class) ||

    clazz.equals(UserResponseInstanceManager.class) ||
    clazz.equals(UserResponseManager.class) ||

    clazz.equals(PocketDiscussionMessageManager.class)
  );
 }
}
That's it.  I know that's a little more complicated than the question you
asked, but for me the notion of aggregates is powerful and useful, and
without knowing your use case I didn't want to go too simple.  However, we
can simplify this to the case where the aggregate boundary is just the
single table.  As I understand your question, you'd like a converter that
will automatically know when a field is outside the table, and use the
foreign key to represent that object.  Correct?

I can think of a few ways to do that, but it depends on how your object
model and your mappings are set up.  If you map one table to one object
(i.e. don't use component mappings), then your canConvert method just needs
to check whether the object to be marshalled is a subclass of your base
class.  If it is, then it's external to the table, canConvert should return
true, and your marshal method can then just write the foreign key to the
xml stream.

If, however, you use complex mappings, there are a couple of further
options.  Firstly, you could create a converter per table.  Not as painful
as it sounds as you could use the one class with a constructor parameter
that tells it which table/object it is dealing with.  I could jot down some
pseudocode if you think this is the way to go.  Secondly, you could inspect
the Hibernate mapping data to work out which fields belong to which table.
I won't go into how to do that here - personally I think that's complicated
enough that you'd want to have a really compelling use case to make it
worthwhile.

Final point, just for completeness.  Deserializing an aggregate is a little
more complex.  Converting the key values back into external object
references generally requires a converter instance per external object
class, as each external object may have a specific way to find it and then
instantiate it if the desired value is missing.  Having done that, the
references for any bi-directional associations have to be re-created in the
existing external objects.  The complexity isn't crippling, but the code is
highly specific to the exact structure of the aggregate being deserialized.

Hope that helps!

Jaime

Reply via email to