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