• Recent Tutorials

  • Tripping The Class Fantastic: Versioned Data Storage

    Example Usage

    So thats the basics... now lets put it to use.

    Code:
    interface
    
    uses streamedObject;
    
    type
      TMyDataStore = class(TStreamedObject)
      protected
        fMyIntData : integer;
        fMyStringData : string;
      public
        property myIntData:integer read fMyIntData write fMyIntData;
        property myStringData:string read fMyStringData write fMyStringData;
      published
        procedure load_ver1(reader:TReader);
        procedure save_ver1(writer:TWriter);
      end;
    
    implementation
    
    procedure TMyDataStore.load_ver1(reader:TReader);
    begin
      fMyIntData:=reader.readInteger;
      fMyStringData:=reader.readString;
    end;
    
    procedure TMyDataStore.save_ver1(writer:TWriter);
    begin
      writer.writeInteger(fMyIntData);
      writer.writeString(fMyStringData);
    end;
    Here we have declared the object whose data we want to store. If you have a single version, its that easy. To use the object to store the data you create a stream and call the appropriate method (loadFromStream or saveToStream) like this.

    Code:
    var
      aStream : TFileStream;
    begin
      // To Save...
      aStream:=TFileStream.create('C:\MyData.dat',fmCreate);
      myDataStore.saveToStream(aStream);
      aStream.free;
    
      // To Load...
      aStream:=TFileStream.create('C:\MyData.dat',fmOpenRead);
      myDataStore.loadFromStream(aStream);
      aStream.free;
    end;
    Just a quick note in case its not obvious, you can of course store multiple objects in a single stream... you just have to be sure you read and write them in the same order.

    Well, thats a basic case... so lets look at version 2. We have added the field 'MyIntData2'.

    Code:
    interface
    
    uses streamedObject;
    
    type
      TMyDataStore = class(TStreamedObject)
      protected
        fMyIntData : integer;
        fMyIntData2 : integer;
        fMyStringData : string;
      public
        constructor create;
    
        property myIntData:integer read fMyIntData write fMyIntData;
        property myIntData2:integer read fMyIntData2 write fMyIntData2;
        property myStringData:string read fMyStringData write fMyStringData;
      published
        procedure load_ver1(reader:TReader);
        procedure save_ver1(writer:TWriter);
    
        procedure load_ver2(reader:TReader);
        procedure save_ver2(writer:TWriter);
      end;
    
    implementation
    
    constructor TMyDataStore.create;
    begin
      inherited;
    
      fObjectVersion:=2;
    end;
    
    procedure TMyDataStore.load_ver1(reader:TReader);
    begin
      fMyIntData:=reader.readInteger;
      fMyStringData:=reader.readString;
    end;
    
    procedure TMyDataStore.save_ver1(writer:TWriter);
    begin
      writer.writeInteger(fMyIntData);
      writer.writeString(fMyStringData);
    end;
    
    procedure TMyDataStore.load_ver2(reader:TReader);
    begin
      load_ver1(reader);
    
      fMyIntData2:=reader.readInteger;
    end;
    
    procedure TMyDataStore.save_ver2(writer:TWriter);
    begin
      save_ver1(writer);
    
      writer.writeInteger(fMyIntData2);
    end;
    Notice that now we have 2 versions we have to override the default value of fObjectVersion in the constructor. This object will save the data using the latest version, so it automatically updates data stores when they are saved.

    One thing you will notice is that when you load a version 1 stream, the version 2 fields are not initialised. This can be addressed by initialising them in the version 1 loader, or by modifying the load method to call an initialisation function that sets all fields to a known state.

    Now lets look at the version 3 code. Version 3 removes an item of data. This is a little trickier since we have removed the field that holds the data once it is loaded. The item we will remove is fMyIntData. To achieve this, it is necessary to make a modification to the routines that load and save this item (in this case the load_ver1 and save_ver1 methods) and to declare a new set of load and save code for version 3 and beyond.

    Code:
    interface
    
    uses streamedObject;
    
    type
      TMyDataStore = class(TStreamedObject)
      protected
        fMyIntData2 : integer;
        fMyStringData : string;
      public
        constructor create;
    
        property myIntData2:integer read fMyIntData2 write fMyIntData2;
        property myStringData:string read fMyStringData write fMyStringData;
      published
        procedure load_ver1(reader:TReader);
        procedure save_ver1(writer:TWriter);
    
        procedure load_ver2(reader:TReader);
        procedure save_ver2(writer:TWriter);
    
        procedure load_ver3(reader:TReader);
        procedure save_ver3(writer:TWriter);
      end;
    
    implementation
    
    constructor TMyDataStore.create;
    begin
      inherited;
    
      fObjectVersion:=3;
    end;
    
    procedure TMyDataStore.load_ver1(reader:TReader);
    var
      empty : integer;
    begin
      // Since we no longer use fMyIntData, read its value into an empty variable
      empty:=reader.readInteger;
    
      fMyStringData:=reader.readString;
    end;
    
    procedure TMyDataStore.save_ver1(writer:TWriter);
    begin
      // Write an empty value (just in case we save to version 1)
      writer.writeInteger(0);
    
      writer.writeString(fMyStringData);
    end;
    
    procedure TMyDataStore.load_ver2(reader:TReader);
    begin
      load_ver1(reader);
    
      fMyIntData2:=reader.readInteger;
    end;
    
    procedure TMyDataStore.save_ver2(writer:TWriter);
    begin
      save_ver1(writer);
    
      writer.writeInteger(fMyIntData2);
    end;
    
    procedure TMyDataStore.load_ver3(reader:TReader);
    begin
      fMyStringData:=reader.readString;
      fMyIntData2:=reader.readInteger;
    end;
    
    procedure TMyDataStore.save_ver3(writer:TWriter);
    begin
      writer.writeString(fMyStringData);
      writer.writeInteger(fMyIntData2);
    end;
    As you can see, removing items is trickier than adding, but I've found that I generally remove a lot fewer items than I add. Of course you can minimise your work by thinking ahead and designing your code before you get into the nuts and bolts of creating data stores. But if you do dive straight in and you find yourself with a lot of redundant items, I would advise removing them together. The reason is that everytime you remove an item, you need to rewrite the majority of the load_verx/save_verx code as shown in the example above. You will notice that when we add items we call the earlier routines (load_ver2 calls load_ver1 etc). But when we remove items its necessary to break the chain at the version they were removed in (so load_ver3 doesn't call earlier versions), this obviously necessitates writing a whole bunch of code that loads/saves all the fields you haven't removed.

    That concludes this little introduction to dynamic method calls and their practical application in constructing versioned data stores. I hope you can see that there are many possibile applications of this ability to call methods dynamically at runtime, but care should be taken when considering whether it is appropriate. I haven't conducted any timings but my gut feeling is that this is not the quickest way in the world. Depending on your application, a big ugly IF..THEN..ELSE or CASE... may be a better approach. But where speed isn't important and you're IF..THEN..ELSEs are getting out of hand, this may be applicable.

    With regards to the data storage method presented here, there are other ways to achieve this kind of thing, but with this system you have full control over whats saved and how, you don't have to expose every field as published/public in order to store it and above all, its easy to understand... even for beginners.

    As usual, if you have any questions or comments, then please feel free to email me on athena at outer hyphen reaches dot com or post a comment on the article. Thanks for reading. Until next time... take care and happy coding

    2010 Update - This article did receive some comments and questions which were answered on this thread.
    Comments 2 Comments
    1. Brainer's Avatar
      Brainer -
      Very nice one.

      Regarding TReader and TWriter - in the latest Delphi version you can write class helpers, so it's possible to expand the functionality of TStream and add your own methods for, let's say, storing and reading back a floating-point, while still having the possibility to store a stream in another one.
    1. AthenaOfDelphi's Avatar
      AthenaOfDelphi -
      Thanks Brainer