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 J**** > > ** ** > > 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**** >
