Sneak preview about DORM, The Delphi ORM

Posted on mer 01 settembre 2010 in Design Patterns, Programming, RTTI

My yesterday post about this busy time, have raised some interest about DORM, the Delphi ORM.

So, also if I still haven't released any files, wish to expose some internals about DORM.

DORM is an implementation of the DataMapper design pattern written having Hibernate in mind.

It's completely unit tested and have the following features:

  • External file mapping. (JSON format)
  • Persistence ignorance (every TObject can be persisted)
  • Support for One-One and One-Many relations (still no many-many)
  • Support for LazyLoading (you can enable§/disable lazyloading by file or by code by per-basis needs)
  • Support for IdentityMap
  • Support for custom "finder" (you can still use complex SQL if you want)
  • Complete support for CRUD
  • Transactions
  • Built in logging system to log *EVERY* sql or action performed by the framework
  • Opened to multiple data access strategies (interfaced based, not inheritance based) for use with different database (now I've developed the firebird one using DBX)
  • Caching for RTTI (the TSession object have a single TRttiContext holding ALL metadata)

Code is still under heavely development.

Those are 2 test-method to show the use of DORM:

``` {lang="delphi"} TPerson = class(TObject) ... property Phones: TdormCollection.... //implements IList end;

TPhone = classs(TObject) ... end;

//and now the unit tests

procedure TTestDORMHasMany.Setup; begin Session := TSession.Create; Session.Configure(TStreamReader.Create('dorm.conf')); end;

procedure TTestDORMHasMany.TearDown; begin Session.Free; end;

procedure TTestDORMHasMany.TestHasManyLazyLoad; var p: TPerson; t: TPhone; guid: string; begin p := TPerson.NewPersona; //static method. Return a fully populated TPerson object try t := TPhone.Create; p.Phones.Add(t); Session.Save(p); guid := p.guid; //GUIDs, or other PK types, are generated automagically by DORM. Obviously there is a specific class loaded to do this specified in the dorm.conf file) finally Session.Commit; end; Session.StartTransaction;

// Test with lazy load ON Session.SetLazyLoadFor(TypeInfo(TPerson), 'Phones', true); p := Session.Load(TypeInfo(TPerson), guid) as TPerson; try CheckEquals(0, p.Phones.Count); finally Session.Commit; end;

Session.StartTransaction; // Test with lazy load OFF Session.SetLazyLoadFor(TypeInfo(TPerson), 'Phones', false); p := Session.Load(TypeInfo(TPerson), guid) as TPerson; // Without commit, AV becouse IdentityMap doesn't work properly try CheckEquals(1, p.Phones.Count); // Child objects are loaded finally Session.Commit; end; end;

procedure TTestDORMHasMany.TestLoadHasMany; var list: IList; t, t1: TPhone; p: TPerson; guid: string; begin p := TPerson.NewPersona; //static method. Return a fully populated TPerson object try t := TPhone.Create; t.Numero := '555-7765123'; t.Kind := 'Casa'; p.Phones.Add(t);

t1 := TPhone.Create;
t1.Number := '555-7765123';
t1.Kind := 'Casa';
p.Phones.Add(t1);
Session.Save(p); // save Person and Phones
guid := p.guid;

finally Session.Commit; end;

Session.StartTransaction; p := Session.Load(TypeInfo(TPerson), guid) as TPerson; try CheckEquals(2, p.Phones.Count); finally Session.Commit; end; end;

Mapping, contained in a file called "dorm.conf", is similar to the following:

``` {lang="javascript"}
{
  "persistence": {
    "database_adapter": "dorm.adapter.Firebird.TFirebirdPersistStrategy",
    "database_connection_string":"127.0.0.1:C:\MyProjects\DORM\experiments\dorm.fdb",
    "username": "sysdba",
    "password":"masterkey"
    },
  "config": {
    "package": "dorm.bo.Person",
    "logger_class_name": "dorm.loggers.FileLog.TdormFileLog"
  },      
  "mapping":
    {      
      "TPerson":
      {
        "table": "people",
        "id": {"name":"guid", "field":"guid", "field_type":"string", "size": 100, "default_value": ""},
        "fields":[
          {"name":"firstname", "field":"first_name", "field_type":"string", "size": 100},
          {"name":"lastname", "field":"last_name", "field_type":"string", "size": 100},
          {"name":"age", "field":"age", "field_type":"integer"},
          {"name":"borndate", "field":"born_date", "field_type":"date"}
          ],
        "has_many":[{
          "name": "Phones",
          "class_name":"TPhone",
          "child_field_name":"guid_person",
          "lazy_load": false
        }],
        "has_one": {
          "name": "car",
          "class_name":"TCar",
          "child_field_name":"guid_person"
        }
      },
      "TPhone":
      {
        "table": "phones",
        "id": {"name":"guid", "field":"guid", "field_type":"string", "size": 100, "default_value": ""},
        "fields":[
          {"name":"number", "field":"number", "field_type":"string", "size": 100},
          {"name":"kind", "field":"kind", "field_type":"string", "size": 100},
          {"name":"guid_person", "field":"guid_person", "field_type":"string", "size": 100}
          ]
      }
    }
}

The PODO (Plain Old Delphi Objects) can be binded to the VCL controls with a set of MediatingView (Model-GUI-Mediator Pattern) with an Observer mechanism to mantain things in synch.

Any comments? Someone interested?