Hi,
I'm currently learning about typehandlers, because I intend to implement a custom typehandler for some of my classes. (Why I want a different handler instead of the default FirstClassObjectHandler doesn't matter for now. Let's say I want to improve it. ;-) ) I went quite deeply into the sources of db4o and did pretty much debugging to try to understand how it actually works.
As part of my experiments I found a strange behaviour of db4o and its handling of object reading/writing from and to the database. I created a simple test case to demonstrate this strange operation (I used db4o-7.4.49.11005 for Java):
http://www.sendspace.com/file/5dtef8
- I have two simple classes A and B. A references B. Both classes have some primitive fields (String and int) as well.
- I created MyObjectTypeHandler as a subclass of FirstClassObjectHandler. I overriden two TypeHandler4 methods: write() and read(). These two methods simply write and read a byte ((byte)'X' = 88) and then pass control to FirstClassObjectHandler to do the actual writing and reading of the object.
- I registered two separate MyObjectTypeHandlers: one for A and one for B (internally db4o does the same: registers a separate FirstClassObjectHandler for every class, including these classes' superclass)
- I created an ObjectContainer with TransparentPersistenceSupport.
- Compiled the whole thing and enhance A and B using com.db4o.instrumentation.ant.Db4oFileEnhancerAntTask
I run two tests with clean builds and with an empty (non existing) db file ("typehandlertest.db").
1. Storing objects without update
- I create two objects 'a' and 'b' (of classes A and B respectively)
- Store them in the ObjectContainer
- Close and reopen the ObjectContainer
- Load back 'a' by its ID.
This test runs fine. MyObjectTypeHandler's read() and write() are invoked, they write the byte and read it back perfectly when the ObjectContainer is reopened and 'a' is reloaded.
2. Storing objects with update
- I create two objects 'a' and 'b' (of classes A and B respectively)
- Store them in the ObjectContainer
- Change 'a' by setting a new "title" for it. (see main()'s line 27: a.setTitle("Setting a new title for 'a'"))
- Close and reopen the ObjectContainer
- Load back 'a' by its ID.
This test fails. When reloading 'a', MyObjectTypeHandler's read() is invoked but it read 0 instead of 88 ('X') and later fails with
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 8195
at com.db4o.internal.ByteArrayBuffer.readInt(ByteArrayBuffer.java:155)
Read byte:
at com.db4o.internal.AbstractBufferContext.readInt(AbstractBufferContext.java:60)
at com.db4o.internal.handlers.StringHandler.readStringNoDebug(StringHandler.java:220)
at com.db4o.internal.handlers.StringHandler.readString(StringHandler.java:212)
at com.db4o.internal.handlers.StringHandler.read(StringHandler.java:235)
at com.db4o.internal.marshall.AbstractReadContext.readAtCurrentSeekPosition(AbstractReadContext.java:68)
at com.db4o.internal.marshall.AbstractReadContext$1.run(AbstractReadContext.java:52)
at com.db4o.internal.marshall.SlotFormat.doWithSlotIndirection(SlotFormat.java:99)
at com.db4o.internal.marshall.AbstractReadContext.readObject(AbstractReadContext.java:50)
at com.db4o.internal.marshall.AbstractReadContext.read(AbstractReadContext.java:45)
at com.db4o.internal.FieldMetadata.read(FieldMetadata.java:858)
at com.db4o.internal.FieldMetadata.instantiate(FieldMetadata.java:656)
at com.db4o.internal.handlers.FirstClassObjectHandler$1.processField(FirstClassObjectHandler.java:81)
at com.db4o.internal.handlers.FirstClassObjectHandler.traverseFields(FirstClassObjectHandler.java:212)
at com.db4o.internal.handlers.FirstClassObjectHandler.instantiateFields(FirstClassObjectHandler.java:84)
at com.db4o.internal.handlers.FirstClassObjectHandler.read(FirstClassObjectHandler.java:111)
at typehandlertest.MyObjectTypeHandler.read(MyObjectTypeHandler.java:35)
at com.db4o.internal.ClassMetadata.read(ClassMetadata.java:1896)
at com.db4o.internal.ClassMetadata.instantiateFields(ClassMetadata.java:1187)
at com.db4o.internal.ClassMetadata.activate(ClassMetadata.java:1109)
at com.db4o.internal.ClassMetadata.instantiate(ClassMetadata.java:1073)
at com.db4o.internal.marshall.UnmarshallingContext.readInternal(UnmarshallingContext.java:112)
at com.db4o.internal.marshall.UnmarshallingContext.read(UnmarshallingContext.java:71)
at com.db4o.internal.ObjectReference.read(ObjectReference.java:320)
at com.db4o.internal.ObjectReference.readForActivation(ObjectReference.java:160)
at com.db4o.internal.ObjectReference.activateInternal(ObjectReference.java:156)
at com.db4o.internal.ObjectReference.activate(ObjectReference.java:135)
at com.db4o.internal.ObjectReference.activateOn(ObjectReference.java:90)
at com.db4o.internal.ObjectReference.activate(ObjectReference.java:77)
at typehandlertest.A.activate(A.java)
at typehandlertest.A.toString(A.java:40)
at java.lang.String.valueOf(String.java:2615)
at java.lang.StringBuilder.append(StringBuilder.java:116)
at typehandlertest.Main.main(Main.java:45)
Debugging this code and db4o I found a strange way how typehandlers are invoked.
When an object is stored (like 'a' in my case) MyObjectTypeHandler is invoked both for each object that is referenced by the root object that has been stored (in my case 'a' and 'b'). In this initial store case the typehandler (FirstClassObjectHandler) takes care of traversing the fields of the objects and writing them to a byte array.
When an object is updated (in my case 'a') the typehandlers are only invoked on the referenced objects (the "title" and "b" fields) but not on the changed object itself! In this update case the ObjectMarshaller takes care of traversing the changed object's ('a') fields and only invokes the typehandlers for the referenced objects.
I would be very happy If someone with some deeper knowledge on the internals of db4o could help me to figure out why my code breaks when updating object 'a' and why not when it is just simply stored.
Could this duplicate way of traversing the objects in case of simple store and update (FirstClassObjectHandler vs. ObjectMarshaller) cause the problem? Shouldn't ObjectReference.writeUpdate() use the typehandlers (FirstClassObjectHandler)
to traverse the graph?
Or when 'a' is updated it maybe written to a different place in the file and therefore my 'X' byte gets "lost"?
It seems that the TypeHandler4 inerface and typehandlers in general are the future in the life of db4o (http://developer.db4o.com/blogs/product_news/archive/2008/08/23/wanted-typehandlers.aspx), but to help in implementing them efficiently we should understand them better. ;-)
Thanks for any hints in advance!
Regards,
---
balazs