Home » Design Patterns » Programming » RTTI » Sneak preview about DORM, The Delphi ORM

Sneak preview about DORM, The Delphi ORM

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:

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:

{
  "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?

25 commenti a “Sneak preview about DORM, The Delphi ORM

  1. Looks good and promising! Would really like a good ORM for Delphi!

    Some initial comments/questions:
    – why Session.Commit in finally, no initial Session.StartTransaction?

    – how about generics? like “p := Session.Load” is much nicer to read and to code!

    – how about attributes?
    [Table = ‘people’]
    class TPerson
    [LayLoad=True]
    property Phones: TdormCollection
    I prefer attributes instead of external mapping, would be nice to have both to be flexible whatever anyone likes

    – how about a class generator from database? My current customer does this with their own data objects, and it is really useful with many tables and many db changes!

    I hope you develop with above points in mind, even if this is limited to D2010+, otherwise your DORM will be old and outdated before it is released…
    I know there are a lot D7 programmers and apps, but after working a half year with D2010 I will never want back to old and outdated D7!

    IMHO, Delphi needs a modern(!) ORM to compete with .Net, not an old legacy Delphi compatible thing which does not make use of latest technologies (so it is not easy to use)

    I also saw some partial LINQ implementation with generics in DeHL!

  2. @André:
    Q: why Session.Commit in finally, no initial Session.StartTransaction?
    A: Create a Session imply a new transaction start, so needs the commit.

    Q: how about generics? like “p := Session.Load” is much nicer to read and to code!
    A: Yes, is under consideration. Could be something like the following
    “P:= Session.Load(3);”

    Q: how about attributes?
    [Table = ‘people’]
    class TPerson
    [LayLoad=True]
    property Phones: TdormCollection
    I prefer attributes instead of external mapping, would be nice to have both to be flexible whatever anyone likes

    A: Attributes are nice but are limited to simple mapping. How you can mapp 3 object in a simgle table with attributes mapping? How map a single object in 3 tables? External file mapping is very powerfull and extendible. However, Attributes could be used for simple mapping.

    Q: how about a class generator from database? My current customer does this with their own data objects, and it is really useful with many tables and many db changes!

    A: Already in the roadmap. The start point for the DORM will be always the mapping file. But that file can be generated from DB and can generate the Delphi code.

    I REALLY want a modern ORM. So I need the NEW RTTI. With the new RTTI is possibile to write a decent and modern ORM in a reasonable time. SOuld be very nice if some programmer (hopefully with Hibernate/NHibernate exterience could help me in this effort).

    ASAP I’ll post other details.

  3. - session.create is implicit startransaction? Wouldn’t it be better to name it “session.CreateAndStartTransaction”? otherwise everybody will forget to commit!

    – for my customer I need simple mapping :-) so attributes would be good enough (better than seperate file!) but for complex mapping you need mapping file yes.

    Also think about version control: because I heard this is a (very) weak point of the ADO.Net ORM: 3 seperated xml files for mapping with bad version control possibilities (merge, compare, etc)

    I would like to help, but I have no no hibernate experience and little spare time (maybe more in the winter :-). But can help thinking and try some techniques (I will refactor my customers data objects in near future, so maybe there is some things we can share)

    And yes, you will need the new RTTI for this! Very nice new feature!

  4. Looks really nice!
    Ill second use of attributes/rtti instead of external file.

    Have You thought about something like this:
    if t.Numero = null then…
    in my “ORM” object fields are objects not simple type fields, so i can do p.Numer.IsNull…

  5. @Magno Machado Paulo
    lazy-load is not restricted to TdormCollection.
    Now there is only this possibility, but the lazyloading is designed for using in *EVERY* association.

  6. @misiak:
    Nullable types should be a good thing for Delphi. DORM can use every value-type-compatible property from the RTTI POV.
    There is an alternative way to dealing with null values: using a default value for the value. This “null-value” is configured in the dorm.conf file. So your code could become:
    If t.numero = -1 then … //-1 is configured as “NULL” value for the field “NUMERO”

    Is not elegant as nullable-types bnut i works.

  7. Looks very promising. One question though: why don’t you use an xml file configuration for dorm.conf? Why re invent the wheel?

  8. @Andrew Tierney:
    No, still there isn’t a public package, Probably I’ll put the project in googlecode.

    @justin:
    JSON is a standard, it is less verbose than XML and it is more fast to parse. There are opensource parser for it and there is the Delphi 2010 one. So, I dont reinvent the wheel… I’ve only “changed” the usually wheel J

  9. @Jeroen Pluimers:
    * DORM haven’t support for design time stuff. It is my choice.
    * hOPF AFAIK require a LayerSupertype (http://martinfowler.com/eaaCatalog/layerSupertype.html), DORM dont.
    * DORM haven’t a built-in “binding” system. There is an external framework based on the MGM (Model-GUI-Mediator) design pattern that can do that.
    * DORM is *HEAVELY* based upen interfaces, AFAIK hOPF dont.
    * hOPF is ccompatible with old versions, DORM require D2010+
    * hOPF if a finished project with years of history, currently DORM is a *nice* working-experiment.

    My knowledge about hOPF could be not sufficient to do a deeper comparison.

  10. How did you solve the LazyLoading problem, if Delphi does not have the facility of dynamic proxy?

  11. What a way to implement the design pattern LazyLoad did you use?

    Virtual proxy, Ghost, Value holder?

    Many thanks.

I commenti sono disattivati.