OK,

I have it working now with a slightly different solution as state before:

public void export()
{
        ZipOutputStream zip;
        List<Class<?>> entityClasses;
        List<Class<?>> annotations = new ArrayList<>();
        final int batchSize = 50;
        Number count;
        Criteria query;
        XStream stream;
        EntityExternalRefExportConverter converter;

        try
        {
                stream = createXstreamInstance();
                converter = new 
EntityExternalRefExportConverter(stream.getMapper(), 
stream.getReflectionProvider());
                entityClasses = 
ReflectUtil.getClasses(BaseEntity.class.getPackage().getName(), null, 
annotations, false);
                annotations.add(Entity.class);
                zip = new ZipOutputStream(new FileOutputStream(zipFile), 
Charset.forName("utf-8"));
                session = sessionFactory.openSession();

                stream.registerConverter(converter);

                for (Class<?> clazz : entityClasses)
                {
                        query = session.createCriteria(clazz);
                        query.setProjection(Projections.rowCount());

                        count = (Number) query.uniqueResult();

                        query = session.createCriteria(clazz);

                        zip.putNextEntry(new ZipEntry(clazz.getSimpleName() + 
".xml"));

                        if (count != null)
                        {
                                for (int i = 0; i < count.intValue(); i += 
batchSize)
                                {
                                        query.setFirstResult(i);
                                        query.setMaxResults(batchSize);

                                        for (Object object : query.list())
                                        {
                                                try
                                                {
                                                        
converter.setId(((BaseEntity) object).getId());

                                                        stream.toXML(object, 
zip);
                                                }
                                                catch (Exception e)
                                                {
                                                        e.printStackTrace();
                                                }
                                        }
                                }
                        }
                }

                zip.close();
        }
        catch (Exception e)
        {
                e.printStackTrace();
        }
        finally
        {
                session.close();
        }
}

class EntityExternalRefExportConverter extends ReflectionConverter
{
        public EntityExternalRefExportConverter(final Mapper p_mapper, final 
ReflectionProvider p_reflectionProvider)
        {
                super(p_mapper, p_reflectionProvider);
        }

        private Object id;

        public Object getId()
        {
                return id;
        }

        public void setId(final Object id)
        {
                this.id = id;
        }

        @Override
        public void marshal(final Object p_object, final 
HierarchicalStreamWriter p_writer, final MarshallingContext p_context)
        {
                if (id.equals(((BaseEntity) p_object).getId()))
                {
                        super.marshal(p_object, p_writer, p_context);
                }
                else
                {
                        p_writer.setValue(((BaseEntity) p_object).getId());
                }
        }

        @Override
        public Object unmarshal(final HierarchicalStreamReader p_reader, final 
UnmarshallingContext p_context)
        {
                throw new Error("UnitExternalRefExportConverter should not be 
used for unmarshalling.  Use UnitExternalRefImportConverter instead");
        }

        @Override
        public boolean canConvert(final Class p_clazz)
        {
                return BaseEntity.class.isAssignableFrom(p_clazz);
        }
}

As you can see I actually have a per-instance converter which knows the unique 
id of the current object being serialized. This id is being set in each loop 
cycle. If the converter faces a domain object that is not the instance 
currently being serialized (top-level) it marshalls it just with its id. 
Otherwise the marshaling is delegated to the built-in ReflectionConverter. This 
works only if your domain objects id is a globally unique identifier or at 
least unique within your domain object. This is true if you use a UUID for 
example or some database specific unique identifier type like sql servers 
uniqueidentifier type. Otherwise the id set in the converter must have the 
table information as well.

Hope someone can get use this.

Hang on,

Sebastian


Von: Jaime Metcher [mailto:[email protected]] 
Gesendet: Mittwoch, 17. Oktober 2012 23:30
An: [email protected]
Betreff: Re: [xstream-user] Hibernate associations

Hi Sebastian,
 
So you're wondering, if your top-level object that you pass to xstream is 
itself a subclass of your base class, will the converter reject the top-level 
object so you just end up with an ID in your output?  Good question.  I think 
your guess is correct, i.e. the converter is only called on the properties of 
the top level object, not the top level object itself.  
 
I'm not able to write a test case right now, so I base that on a reading of the 
xstream 1.3.1 code.  That may have changed for 1.4 or I might be misreading, so 
it's worth testing this.
Somewhat OT: 
 I think you might be misusing the term "aggregate" here.  Maybe you're using 
the term to mean "foreign key relationship" or "foreign object"?  The 
definition of an aggregate boundary is purely to do with the conceptual object 
model and in general there's nothing in the structure of the tables or classes 
that will tell you what's in and what's out.  As opposed to a foreign key 
relationship, which is purely structural and in general doesn't tell you 
anything about the conceptual model.  There might be a really reductive case 
where the two coincide, but I haven't seen it.  The code I posted is to do with 
serializing at the conceptual aggregate level (hence the hardcoded list of 
classes), but in the paragraph you quoted we've flipped over to the other 
approach, serializing at the structural table-by table level - so the term 
"aggregate" doesn't really apply.  Sorry if that sounds picky :)
 
Jaime
On Wed, Oct 17, 2012 at 9:57 PM, Sebastian Götz <[email protected]> wrote:
Thank you Jaime for the detailed explanation.
 
What I do not understand is what you tell:
 
“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.”
 
How do I know that the current class is an aggregate? Indeed we have a table 
per domain object model. I know nothing about the hierarchical processing of 
the xstream driver. But I’ve I guess correctly, than you mean that your 
UnitExternalRefExportConverter will only get called on the properties of a 
domain object. And so if it detects a subclass of the BaseDomainObject class it 
knows that there is an aggregate. Right?
 
Thanks again and excuse my English ☺
 
Regards
 
Sebastian
 
Von: Jaime Metcher [mailto:[email protected]] 
Gesendet: Mittwoch, 17. Oktober 2012 00:39

An: [email protected]
Betreff: Re: [xstream-user] Hibernate associations
 
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



---------------------------------------------------------------------
To unsubscribe from this list, please visit:

    http://xircles.codehaus.org/manage_email


Reply via email to