The subject of object or schema migration is quite large, and I'm hardly an expert on most of it. There is an area I can talk about with some authority though - migrating old versions of objects forward when using BOSS. Here's the situation - say you save objects to disk (what Java developers would call serializing them). Flash forward a bit, with the objects still on disk, but the definition of the object having changed (i.e., you added or removed instance variables). So, for instance, say you had this when you saved the objects to disk:
Smalltalk defineClass: #Item
instanceVariableNames: 'read guid '
and you changed it to this:
Smalltalk defineClass: #Item
instanceVariableNames: 'read guid category'
Well, now there's an issue - the old objects had 2 instance variables, the new one has three. What do you do? Well, you create some class side code that tells the BOSS framework how to migrate the objects forward:
"current version number for BOSS"
binaryReaderBlockForVersion: oldVersion format: oldFormat
" An attempt is being made to read instances of
an obsolete version of this class. Answer a block
that converts old instances (represented as an array
or string of instance variable values) to new ones."
oldVersion == nil
ifTrue: [[^self nilBinaryMigrationBlock].
The first method answers the version number - this will be encoded with every object you save in BOSS format. When it's read in, this version number is available (it's nil if you never assigned it). The second method determines how to handle migrations - in this case, by sending a message based on the inbound version. That method - nilBinaryMigrationBlock above - actually contains the block that will do the migration. Given the examples we started with, it might look like this:
| inst |
inst := self basicNew.
| newObj sz array|
sz := self allInstVarNames size.
newObj := Array new: sz.
to: oldObj size
oldObj become: newObj.
oldObj changeClassToThatOf: inst].
What does that do? It creates an array (which matches the instance variable slots), and then copies all the old attributes into the array up to the old object's size. Now, this method is written somewhat naively - on the assumption that there are N new attributes. If you dropped attributes or changed their order, you would have to write the code based on that. Still, this gives you an idea of what's happening. What you end up with is a new object, with all the old object's data - see the #become:.
The beauty of this is that you can do it with live systems - more than once, I've had to change the shape of the object used for blog entries - and I've managed that with schema migration. It's a powerful technique - but also easy enough to trip over. If you change the shape of an object rapidly (as you initially develop an application, for instance, or under rapidly changing requirements) - it's easy enough to get the older migration methods wrong (especially if you are sometimes dropping attributes). If you need to do this with a production system with important data, testing is absolutely required