Akimov Alexey > ORM-0.85.1 > ORM::Tutorial

Download:
ORM-0.85.1.tar.gz

Annotate this POD

CPAN RT

New  1
Open  1
View/Report Bugs
Source  

NAME ^

ORM::Tutorial - Guided tour to ORM module.

SYNOPSIS ^

Purpose of this document is to brief introduce usage of PerlORM library on simple example. Example is 'Tasks Planner' (or 'Todo List') application.

OBJECT MODEL ^

Let's start with simple object model, which will be improved and modified as needed later. Object classes of our example application are:

1. Task

Properties:

  • Title (title)
  • Detailed description (desc)
  • Creation time (created)
  • Start time (start_date), can be undef
  • End time (end_date), can be undef
  • Deadline (deadline), can be undef
  • Responsible worker (worker)
2. Worker

Properties:

  • Worker name (name)

First step in creation of object model is to create so called initial class. Initial class is base class for all classes of our object model.

File Todo/ORM.pm

  package Todo::ORM;
  
  use ORM::Db::DBI::MySQL;
  use base 'ORM';
  
  Todo::ORM->_init
  (
      prefer_lazy_load     => 0,
      emulate_foreign_keys => 1,
      default_cache_size   => 200,
  
      db => ORM::Db::DBI::MySQL->new
      (
          host        => 'localhost',
          database    => 'todo_list',
          user        => 'root',
          password    => '',
      ),
  );
  
  1;

Now let's declare classes of our model.

PerlORM was developed with usage simplicity in mind therefore it does not require you to declare class properties and relations in both class declaration and database. Creation of database table for storing objects of the class is quite enough. Fields in this table are correspond to object properties.

Also there is tool-script in development that allows to screate database tables and templates of Perl-modules from UML schemes, but if you doesn't want to spend time to build additional class or database schemes you are always able to use short way.

One or more database tables are assigned to each class (more than one table is used in case of inheritance). Each object of the class is represented by single row in table or inner join of rows in case of inheritance.

Initial declaration of classes looks very simple:

File Todo/Task.pm

  package Todo::Task;
  
  $VERSION=0.1;
  
  use ORM::Base 'Todo::ORM';

File Todo/Worker.pm

  package Todo::Worker;
  
  $VERSION=0.1;
  
  use ORM::Base 'Todo::ORM';

There is one question: how PerlORM detects what table to use for certain class? If table name is not specified obviously then ORM class calls method _guess_table_name which is by default uses regexp $class =~ s/::/_/g; to detect table name from class name. You can change this behaviour by overriding _guess_table_name method in your initial class. For example:

  sub _guess_table_name
  {
      my $my_class = shift;
      my $class = shift;
      my $table;
  
      $table = substr( $class, index( $class, '::' )+2 );
      $table =~ s/::/_/g;
  
      return $table;
  }

Now table for class Todo::Task should be named Task and not Todo_Task.

It's time to create database tables. (Name of database being used is specified in storage driver constructor.)

  CREATE DATABASE todo_list;
  
  DROP TABLE IF EXISTS `todo_list`.`_ORM_refs`;
  CREATE TABLE `_ORM_refs` (
      `class` varchar(45) NOT NULL default '',
      `prop`  varchar(45) NOT NULL default '',
      `ref_class` varchar(45) NOT NULL default '',
      PRIMARY KEY  (`class`,`prop`)
  ) ENGINE=InnoDB;
  
  INSERT INTO `_ORM_refs` VALUES ( 'Todo::Task', 'worker', 'Todo::Worker' );
  
  DROP TABLE IF EXISTS `todo_list`.`Task`;
  CREATE TABLE `task` (
      `id` bigint(20) unsigned NOT NULL auto_increment,
      `title` varchar(255) NOT NULL default '',
      `desc` text NOT NULL,
      `created` date default NULL,
      `start_date` date default NULL,
      `deadline` date default NULL,
      `worker` bigint(20) unsigned default NULL,
      PRIMARY KEY  (`id`)
  ) ENGINE=InnoDB;
  
  DROP TABLE IF EXISTS `todo_list`.`Worker`;
  CREATE TABLE `worker` (
      `id` bigint(20) unsigned NOT NULL auto_increment,
      `name` varchar(100) NOT NULL default '',
      PRIMARY KEY  (`id`)
  ) ENGINE=InnoDB;

We just created 3 tables, first of them _ORM_refs is special table. ORM uses it to detect relations between classes in our model and usage of third party classes. In our model worker property of class Todo::Task should be reference to an object of class Todo::Worker. To tell it to ORM we should insert following row in _ORM_refs:

  class       | prop      | ref_class
  ------------------------------------
  Todo::Task  | worker    | Todo::Worker

This is the usual way the relations between classes are defined. Note that when you define reference property in Todo::Task class to Todo::Worker, the second one automatically gets so called reversive property or collection property that contains all tasks assigned to particular worker object. In RDBMS terms such kind of relation is called one-to-many.

For frequently used classes there is another way to define relations between objects, this way is to override ORM::_db_type_to_class method in our initial class. _db_type_to_class method accepts table field name and type as its arguments and returns class that should be assigned to property.

The default behavior of _db_type_to_class method defined in ORM class is to assign classes ORM::Date and ORM::Datetime to properties described by fields of type DATE and DATETIME respectively. This behaviour is also can be changed to use another classes to represend dates and times, the chapter "THIRD PARTY CLASSES" tells how to do it.

Every table that is used with ORM should have autoincremented field id which stored ID of objects of corresponding class. (Functionality that allows to use arbitrary column or multiple columns to represent object ID is subject to furthrer development.)

CREATING OBJECTS ^

Creation of objects in ORM is performed by calling 'new' method. For example let's create 'Worker' object:

  use Todo::Worker;
  
  $error  = ORM::Error->new;
  $worker = Todo::Worker->new
  (
      prop  => { name => 'E. Cartman' },
      error => $error,
  );
  
  print $error->text;

If 'new' operation fails then $error object contains information about occured errors. Use of $error object is not necessary but strongly recomended.

Now to more easily manage objects of our model we will create perl script for object creation new.pl

File new.pl

  #!/usr/bin/perl
  #
  # Use: perl new.pl <Class> <Prop1Name> <Prop1Value> <Prop2Name> <Prop2Value>...
  #
  # Class - Name of the class without 'Todo::' prefix.
  #
  
  use lib "lib";
  use lib "../ORM/lib";
  
  $nick  = shift;
  $class = "Todo::$nick";
  
  eval "require $class" or die $@;
  
  $error = ORM::Error->new;
  %prop  = @ARGV;
  $obj   = $class->new( prop=>\%prop, error=>$error );
  
  if( $obj )
  {
      print "New $nick was created with id:".$obj->id."\n" if( $obj );
      $obj->print;
  }
  
  print $error->text;

Above script uses print method we doesn't declare yet. This method is aimed to print plain text information about specified object. This method should be defined in initial class so every object of our model can acces it.

  sub print
  {
      my $self  = shift;
      my $ident = shift||0;
      my @ref;
  
      # Do not dive deeper that third level of recursion
      # when printing information about related objects.
  
      return if( $ident > 3 );
  
      # Print information about specified object
  
      print ' 'x($ident*2),('-'x20),"\n";
      for my $prop ( (ref $self)->_all_props )
      {
          printf "%".(20+$ident*2)."s %s\n", "$prop:",
              (defined $self->_property_id($prop) ? $self->_property_id($prop) : '*UNDEF*');

          if( (ref $self)->_prop_is_ref( $prop ) && $self->_property( $prop ) )
          {
              push @ref, $prop;
          }
      }
      print ' 'x($ident*2),('-'x20),"\n\n";
  
      # Print information about related objects
  
      for my $prop ( @ref )
      {
          print ' 'x(($ident+1)*2),"Related object '$prop':\n";
          $self->_property( $prop )->print( $ident+1 );
      }
  }

Note the way properties were accessed. For this purpose $obj->_property( $property_name ) were used in above code. To access object properties with more grace there is AUTOLOAD method. So you can simply call $obj->$property_name() (or just $obj->deadline for example). The ORM::_property method is for cases when $property_name method should be redefined in child class for some reason.

Also you can use $obj->_property_id( $property_name ) to get raw database value of the property. Its result is:

Now we can fill our model with some more objects.

  # perl new.pl Worker name "Kenny McCormic"
  New Worker was created with id:2
  --------------------
                   id: 2
                class: Todo::Worker
                 name: Kenny McCormic
  --------------------
  
  # perl new.pl Task \
        title "Kill Kenny" \
        desc "Just kill Kenny!" \
        worker 1 \
        created "2005-12-18" \
        start_date "2006-01-01" \
        deadline "2006-01-02"
  
  New Task was created with id:1
  --------------------
                   id: 1
                class: Todo::Task
              created: 2005-12-18
                 desc: Just kill Kenny!
               worker: 1
             deadline: 2006-01-02
                title: Kill Kenny
           start_date: 2006-01-01
  --------------------
  
    Related object 'worker':
    --------------------
                     id: 1
                  class: Todo::Worker
                   name: Eric Cartman
    --------------------
  
  # perl new.pl Task \
        title "Eat Chocolate pie" \
        desc "Ask your mummy." \
        worker 1 \
        created "2005-12-18" \
        start_date "2006-01-01" \
        deadline "2006-01-02"
  
  New Task was created with id:2
  --------------------
                   id: 2
                class: Todo::Task
              created: 2005-12-18
                 desc: Ask your mummy.
               worker: 1
             deadline: 2006-01-02
                title: Eat Chocolate pie
           start_date: 2006-01-01
  --------------------
  
    Related object 'worker':
    --------------------
                     id: 1
                  class: Todo::Worker
                   name: Eric Cartman
    --------------------

For more comfort let's modify Todo::Task class so it can automatically assign current time to created property when explicit value is not specified:

  sub _validate_prop
  {
      my $self = shift;
      my %arg  = @_;
  
      if( $arg{method} eq 'new' && ! $self->created )
      {
          $self->_fix_prop
          (
              prop  => { created=>ORM::Date->current },
              error => $arg{error},
          );
      }
  
      $self->SUPER::_validate_prop( %arg );
  }

Let's add one more task:

  # perl new.pl Task \
        title "Keep alive" \
        desc "Just keep alive!" \
        worker 2 \
        start_date "2005-12-31" \
        deadline "2006-01-02"
  
  New Task was created with id:3
  --------------------
                   id: 3
                class: Todo::Task
              created: 2005-12-18
                 desc: Just keep alive!
               worker: 2
             deadline: 2006-01-02
                title: Keep alive
           start_date: 2005-12-31
  --------------------
  
    Related object 'worker':
    --------------------
                     id: 2
                  class: Todo::Worker
                   name: Kenny McCormic
    --------------------

As you can see created property is implicitly initialized with default value of current time. (It seems like Kenny will die anyway after deadline.)

UPDATING OBJECTS ^

Update of object is performed with the same simplicitly:

  use Todo::Worker;
  
  $error  = ORM::Error->new;
  $worker = Todo::Worker->find_id( id=>1, error=>$error );
  $worker && $worker->update( prop=>{ name=> 'Eric Cartman' }, error=>$error );
  
  print $error->text;

To easily update objects from command line there will be another script that is very similar to new.pl.

File update.pl

  #!/usr/bin/perl
  #
  # Use: perl update.pl <Class> <ObjectID> <Prop1Name> <Prop1Value> <Prop2Name> <Prop2Value>...
  #
  # Class - Name of the class without 'Todo::' prefix.
  #
  
  use lib "lib";
  use lib "../ORM/lib";
  
  $nick  = shift;
  $class = "Todo::$nick";
  
  eval "require $class" or die $@;
  
  $id    = shift;
  $error = ORM::Error->new;
  %prop  = @ARGV;
  $obj   = $class->find_id( id=>$id, error=>$error );
  
  if( $obj )
  {
      $obj->update( prop=>\%prop, error=>$error ) unless( $error->fatal );
      print "Updated $nick with id:".$obj->id."\n";
      $obj->print;
  }
  else
  {
      print STDERR "Object #$id of $class not found!\n";
  }
  
  print $error->text;

SELECTING AND FILTERING ^

Now when we have some tasks planned for workers it's time to make some reports about tasks state. Interesting reports are:

Tasks for first report can be selected as follows:

  ORM::DbLog->write_to_stderr( 1 );
  @tasks = Todo::Task->find
  (
      filter => ( Todo::Task->M->worker == $worker ),
      error  => $error,
  );

Todo::Task->M->worker - is so named Meta-property, Meta-property is object of class ORM::Metaprop or its descendants. In resulting SQL-query Meta-properties are replaced with names of corresponding table fields. Special meta-property Todo::Task->M means object of class Todo::Task itself. Below you will see that meta-properties is very powerful facility and is also easy to use.

Variable $worker should contain Todo::Worker object or just its integer ID.

Variable $error of type ORM::Error contains description of error if any occured during query. error parameter is not required, if it is omitted then error is silently ignored. In future version this behavious can be changed.

Call ORM::DbLog->write_to_stderr( 1 ) enables trace of so called SQL-log to STDERR this is useful tool for debugging you code. In described case (assuming $worker=1) SQL-log trace looks as follows:

  --------------------------
  [Mon Dec 26 00:14:27 2005]: ORM::find: Success
  SELECT
    DISTINCT `Task`.*
  FROM
    `Task`
  WHERE
    (`worker` = '1')

If we need to select tasks by worker name, then call looks like this:

  @tasks = Todo::Task->find
  (
      filter => ( Todo::Task->M->worker->name eq $worker_name ),
      order  => ORM::Order->new( [ Todo::Task->M->created, 'DESC' ] ),
      error  => $error,
  );

Draw attention on using of operators == and eq. Databases usually have no sence to this operator because in most cases they are translated to SQL = operator which is used for both string and numeric comparisons. Nevertheless for best readability it is reasonable to use this operators as in native Perl.

Parameter 'order' specifies that found tasks should be sorted by created time in descendant order.

Let's try little more complicated query when we need to find tasks assigned to workers containing some string in their names:

  @tasks = Todo::Task->find
  (
      filter => ( Todo::Task->M->worker->name->_like( '%Cartman%' ) ),
      order  => ORM::Order->new( [ Todo::Task->M->created, 'DESC' ] ),
      error  => $error,
  );

Resulting SQL-query for the call:

  SELECT
    DISTINCT `_T1_Task`.*
  FROM
    `Task` AS `_T1_Task`
      LEFT JOIN `Worker` AS `_T2_Worker` ON( `_T1_Task`.`worker`=`_T2_Worker`.`id` )
  WHERE
    (`_T2_Worker`.`name` LIKE '%Cartman%')
  ORDER BY `_T1_Task`.`created` DESC

Call for second report looks much similar:

  $M     = Todo::Task->M;
  @tasks = Todo::Task->find( filter => ( $M->deadline < '2006-01-30' ) );

Variable $M is for brevity, such trick is useful when constructing complex meta-expressions.

Filters can be logically grouped with arbitrary complexity using ORM::Expr->_and and ORM::Expr->_or methods. In simplest case you can use overloaded '&' and '|' operators:

  $M     = Todo::Task->M;
  @tasks = Todo::Task->find
  (
      filter =>
        ( $M->deadline < '2006-01-30' )
        & Todo::Task->M->worker->name->_like( '%Cartman%' )
  );

Be careful with operator priorities while constructing expressions with these operators.

There is another interesting report about number of tasks assigned to each worker, for this report we will use stat method, This method is useful when you need info about related objects:

  $M   = Todo::Worker->M;
  $res = Todo::Worker->stat
  (
      data =>
      {
          worker => $M,
          tasks  => $M->_rev( 'Todo::Task' => 'worker' )->_count,
      },
      group_by => [ $M ],
      preload  => { worker=>1 },
  );

Opposite to find method which returns array of objects, stat method returns array of hashes with requested data.

Parameter data is hash reference that defines what kind of data should be retrieved from database. Resulting hash contains records with exactly the same keys as in data parameter and with values retrieved from database as specified by values of data.

In our case $res contains hashes with two keys worker - Todo::Worker object and tasks - number of assigned tasks.

Parameter group_by similar to SQL GROUP BY statement. In resulting SQL-query group_by is replaced with GROUP BY. It is used to define how to apply grouping method _count.

Parameter preload defines objects that should be loaded by resulting query and not later by separate query.

Meta-property $M->_rev( 'Todo::Task' => 'worker' ) so called reversive meta-property. It is used to access objects that refer by one of their property to objects being selected. In our case objects of class Todo::Task referring to objects of class Todo::Worker by property worker, therefore we can reversively access tasks assigned to a worker.

SQL-query for the call:

  --------------------------
  [Mon Dec 26 00:49:34 2005]: ORM::stat: Success
  SELECT
    'Todo::Worker' AS `_worker class`,
    COUNT( `_T2_Task`.`id` ) AS `tasks`,
    `_T1_Worker`.`id` AS `worker`,
    `_T1_Worker`.`name` AS `_worker name`
  FROM
    `Worker` AS `_T1_Worker`
      LEFT JOIN `Task` AS `_T2_Task` ON( `_T1_Worker`.`id`=`_T2_Task`.`worker` )
  GROUP BY `_T1_Worker`.`id`

DELETING OBJECTS ^

Method 'delete' is used for deletion of objects from database.

  $worker->delete( error=>$error );

If 'emulate_foreign_keys' option to ORM::_init method is set to true then before deletion ORM checks if there are another objects that refer to object being deleted. If so $error object contains corresponding error message and object is not deleted.

After object $worker has been deleted from database it is reblessed to be object of class ORM::Broken. Call to any method of this object will croak with error message. This is to be sure that object is not being used after it has been deleted.

ERROR HANDLING ^

Error handling is done by passing 'error' argument to almost every ORM method. 'error' argument should contain object of class ORM::Error. Consider the following code:

  use Todo::Task;
  
  $task  = Todo::Task->find; # first found task
  $error = ORM::Error->new;
  $task->update
  (
      prop  => { worker=>-1 },
      error => $error,
  );
  print STDERR "Failed to update\n" if( $error->fatal );
  print STDERR $error->text;

Output for this code:

  Failed to update
  fatal: ORM->_normalize_prop_to_db_value(): Property 'worker' of type 'Todo::Worker' with id='-1' was not found  

Classes of our object model also can produce their own errors and send them to the caller.

For example we should not permit to set Todo::Task->deadline property to be less than current date. To implement this functionality we will modify Todo::Task class by adding new condition to _validate_prop method:

  if( $self->deadline && $self->deadline < ORM::Datetime->current )
  {
      $arg{error}->add_fatal( "Deadline is in past!" );
  }

As far as _validate_prop method is called only implicitly from 'new' and 'delete' methods, you can be sure that $arg{error} contains correct ORM::Error object. That's why there is no reason to test it before use.

In cases when you not sure that user has passed $error object to your method you can use following scheme:

  sub my_metod
  {
      my $self  = shift;
      my %arg   = @_;
      my $error = ORM::Error->new;
  
      $error->add_fatal( "test 1" );
      $error->add_warn( "test 2" );
  
      $error->upto( $arg{error} );
      return undef;
  }

Call to 'upto' method sends error back to user if $arg{error} contains valid error-object. It is equivalent to $arg{error} && $arg{error}->add( $error ).

In current ORM version all errors are ignored if 'error' argument is not passed to a method. But it seems like a good practice to throw exceptions in such cases. This functionality is to be implemented in future releases without affecting object interface.

META-CLASSES ^

Why do you need meta-classes? Meta-classes give you the way to make meta-objects (or meta-properties) behave almost the same as objects itself. So you can construct meta-expressions the way similar to those for usual Perl-expressions.

(To be documented)

THIRD PARTY CLASSES (To be documented) ^

OBJECT CHANGES HISTORY ^

To enable built-in feature to trace object changes simply use history_class argument to ORM::_init method as shown below:

  Todo::ORM->_init
  (
      history_class => 'Todo::History',
      %another_args,
  );

Next step is to declare Todo::History class. This class behaves as any other regular ORM-class of our model. This means that any change of any object is introduced by one or more 'Todo::History' object. History class declaration is quite simple:

File Todo/History.pm

  package Todo::History;
  
  $VERSION=0.1;
  
  use ORM::Base 'Todo::ORM', i_am_history=>1;
  
  1;

As we have redefined ORM::_guess_table_name method, the table for Todo::History class has assumed name 'History'. Structure of the table should be as follows:

  DROP TABLE IF EXISTS `todo_list`.`History`;
  CREATE TABLE `History` (
  
      `id`        bigint(20)   NOT NULL auto_increment,
  
      `obj_class` varchar(100) NOT NULL default '',
      `obj_id`    int(11)      NOT NULL default '0',
  
      `prop_name` varchar(100) NOT NULL default '',
      `old_value` varchar(255)          default '',
      `new_value` varchar(255)          default '',
  
      `date`      datetime     NOT NULL,
      `editor`    varchar(255) NOT NULL default '',
      `slaved_by` bigint(20) unsigned   default NULL,
  
      PRIMARY KEY  (`id`)
  
  ) TYPE=InnoDB;

From this moment every 'new' (SQL INSERT), 'update' (SQL UPDATE) and 'delete' (SQL DELETE) actions will be logged in 'History' table.

Each action is stored in at least one history object. 'new' action creates exactly one Todo::History object. 'update' and 'delete' actions create more than one Todo::History objects. 'update' action creates number of additional history objects equal to number of properties affected by operation. 'delete' action creates number of additional history objects equal to number of properties of deleted object.

All history objects created for 'delete' or 'update' actions execpt first object has property 'slaved_by' set to id of first history object, this object is called 'master'.

Main use of History class is to store all changes made to objects of our model. But there is one more interesting feature. You can undo changes with 'Todo::History::rollback' method. For example you can restore deleted object by its ID like this:

  $hist = Todo::History->find
  (
      filter => ORM::Expr->_and
      (
          # rollback operation should be called on master history object
          Todo::History->M->master, 
          Todo::History->M->obj_class eq 'Todo::Worker',
          Todo::History->M->obj_id == 1,
      ),
  );
  $hist && $hist->rollback;

This code does the following:

1. Find 'slaved' history objects
2. Create 'Todo::Worker' object with properties from 'slaved' history objects
3. Delete 'slaved' and 'master' history objects

Also it is possible to restore object not by ID but by arbitrary property like this:

  $hist = Todo::History->find
  (
      filter => ORM::Expr->_and
      (
          Todo::History->M->obj_class eq 'Todo::Worker',
          Todo::History->M->prop_name == 'name',
          Todo::History->M->old_value->_like( '%Cartman%' ),
          Todo::History->M->delete_slave,
      ),
  );
  $hist && $hist->slaved_by->rollback;

It is possible to rollback 'new' and 'update' actions by similar fashion. You can find more detailed description of this feature in ORM::History.

CACHING ^

ORM-object caching

Object's in-memory cache implemented on per-primary-class basis. Primary class - is class that is a direct descendant of initial class.

The idea of such cache strategy is to have several caches, by one for every primary class. Objects of non-primary class use cache of its primary class. With that in mind its time to say that default_cache_size argument to ORM::_init specifies cache size for one primary class and not total cache size.

In our object model this means that there is maximum of 200 cached objects for Todo::Worker class and 200 cached objects for Todo::Task class.

You can change cache size of individual primary class this way:

  package Todo::Task;
  
  $VERSION=0.1;
  
  use ORM::Base 'Todo::ORM', cache_size=>1000;

You can get current efficiency of in-memory cache by calling ORM::Cache->total_efficiency and reset the cache counters by calling ORM::Cache->clear_stat

When default_cache_size is set to zero, it is still possible that object will be loaded from cache. This is because of instance caching (see below).

Cache is organized in the fashion when you do not need to purge it manually.

Perl-object caching

This mechanism is mostly common for ORMs and persistence tools in Perl. This means that after

  $task1 = Todo::Task->find_id( id=>1 );
  $task2 = Todo::Task->find_id( id=>1 );

$task1 and $task2 contain the same blessed reference. And even in the following case:

  $task1 = Todo::Task->find_id( id=>1 );
  $task2 = Todo::Task->find( order=>[ Todo::Task->M->id ] );
  $task3 = Todo::Task->find( filter=>( Todo::Task->M->desc->_like( %kill Kenny% ) ) );

All three variables contain the same object.

Perl-object caching works even when cache size is set to 0.

Note: Perl-object caching is performed by means of weaken (see Scalar::Util). Perl-object caching is significantly restricted for the versions of Perl that doesn't support weaken feature. The restrictions are: with cache size set to 0 Perl-object caching doesn't work at all; with cache size set to non-zero value this kind of caching works only for objects those are still in cache's pool queue. Once object is deleted from cache queue, the consequent requests to load the object from the cache will result in 'not cached' responce, even if there are instances of the same object still present in the memory.

Properties caching

When you first access some object-property of ORM-object, then ORM tries to create expected object for you (using find_id for ORM-object properties and __ORM_new_db_value for non-ORM-properties) and then store it in internal object's hash.

When you access the same object-property later it is not created again but fetched from hash.

When you need to refresh object's contents simply call $object->refresh.

TRANSACTIONS ^

The usual way transactions should be used is to set up a correspondence between single method and single transaction. If it is not the case then there is probably some conceptual error take place.

  package Todo::Worker;
  
  # Delegate all worker's tasks to new worker
  sub delegate_tasks
  {
      my $self  = shift;
      my %arg   = @_;
      my $error = ORM::Error->new;
      my $ta    = Todo::ORM->new_transaction( error=>$error );
  
      for my $task ( $self->_rev_prop( 'Todo::Task'=>'worker', error=>$error ) )
      {
          $task->worker( new_value=>$arg{worker}, error=>$error );
      }
  
      $error->upto( $arg{error} );
  }

As you can see the way transactions is used in ORM is different than commonly used in other packages. In ORM you do not need to 'finish' transaction explicitly. This helps you to avoid situations when you forget to call method to finish transaction. $ta contains the object assigned to a newly started transactions. Transaction is finished when $ta object is destroyed. In our case this happens when method is finished.

If $error contain one or more fatal errors when $ta is being destroyed, then transaction is rolled back. Also rollback is performed for active transaction when die function called or runtime error occured.

Note use of '_rev_prop' method in foreach loop:

  $self->_rev_prop( 'Todo::Task'=>'worker', error=>$error );

That is the way reversive properties (see "CREATING OBJECT MODEL") are accessed. If certain reversive property is frequently used then you can define shortcut method for it:

  sub tasks { shift->_rev_prop( 'Todo::Task'=>'worker', @_ ); }

And access it like this:

  @tasks = $self->tasks( error=>$error );
  @tasks = $self->tasks( error=>$error, page=>2, pagesize=>10 );
  $iterator = $self->tasks( error=>$error, return_res=>1 );

In fact the call to _rev_prop first appeared in this chapter is similar to:

  Todo::Task->find( filter=>( Todo::Task->M->worker==$self ), error=>$error );

Transactions in ORM can be nested:

  package Todo::Worker;
  
  # Delegate all old worker's tasks to new worker
  # and delete old worker
  sub discharge
  {
      my $self  = shift;
      my %arg   = @_;
      my $error = ORM::Error->new;
      my $ta    = Todo::ORM->new_transaction( error=>$error );
  
      $self->delegate_tasks( worker=>$arg{worker}, error=>$error );
      $self->delete( error=>$error );
  
      $error->upto( $arg{error} );
  }

As you can see transactional method 'delegate_tasks' is called from another transactional method 'discharge'. Nested transactional object simply not issues transaction related statements to SQL-server. In case when nested transaction is failed then outer transaction is failed too and therefore is rolled back.

It's planned to support transactions emulation for non-transactional databases in cases when it's possible. It's up to ORM users to decide how soon this feature will be implemented.

INHERITANCE ^

Suppose there are tasks that have to be done by group of workers. For that reason Todo::Worker::Group class could be useful that is derivative of Todo::Worker and have additional properties:

1. Team leader
2. Team members

To implement inheritance ORM uses so called vertical mapping. This means that for every child class there is one table must be created which contains only child's direct properties and not contain inheritted properties of base classes. When you do fetch objects of child class it's direct table is joined with tables of all base classes.

This approach is most useful, its only disadvantage compared to another inheritance implementations is that it makes additional overheat to RDBMS to do table joins. But for large heavy-loaded sites this drawback is exhausted by using various types of RDBMS clustering and replication technologies.

To implement Todo::Worker::Group we need following steps:

1. Create class's module
2. Add 'class' property to base class
3. Create DB table for child class

File Todo/Worker/Group.pm

  package Todo::Worker::Group;
  
  $VERSION=0.1;
  
  use ORM::Base 'Todo::Worker';

Note that argument to use ORM::Base is 'Todo::Worker' not 'ORM'.

Now we have to made changes to database:

  CREATE TABLE  `Worker_Group` (
    `id` bigint(20) unsigned NOT NULL auto_increment,
    `leader` bigint(20) unsigned NOT NULL default '0',
    PRIMARY KEY  (`id`)
  ) ENGINE=InnoDB;
  
  INSERT INTO '_ORM_refs' VALUES ( 'Todo::Worker::Group', 'leader', 'Todo::Worker' );
  ALTER TABLE `Worker` ADD COLUMN `class` VARCHAR(45) NOT NULL AFTER `id`;
  UPDATE `Worker` SET class='Todo::Worker';

Now call to Todo::Worker->find returns objects of Todo::Worker and Todo::Worker::Group. To get only Todo::Worker object you should filter the result set like this:

  @workers = Todo::Worker->find( filter=>( Todo::Worker->M->class eq 'Todo::Worker' ) );

The best way to get only Todo::Worker::Group objects:

  @workers = Todo::Worker::Group->find;

Next chapter describes how to implement property of Todo::Worker::Group containing list of group members.

MANY TO MANY RELATIONS ^

In previous chapters one-to-many relation type was shortly described. In this chapter you will get to know how to implement many-to-many relations using ORM.

ORM does not share the way other persistence tools and mappers implement this kind of relations. In fact ORM does nothing to implement it :) Probably because of lazy-loaded author (see below).

The trick here is easy enought. Let's take a look at the nature of many-to-many relations.

To define members of the workers group we should establish relation between Todo::Worker::Group and Todo::Worker. To describe the relation in RDBMS terms we need separate linking table called e.g. 'worker_to_group' or in worst case 'workers_to_groups' (avoid to use plural form in class names). Roughly the same we need when using ORM but the table is assigned to linking class, let's call it Todo::Worker::GroupMember. (I usually use pair of colons to separate child classes so Todo::Worker::Group::Member is not so good, but this is just my subjective rule).

Todo::Worker::GroupMember class is nothing else but plain ORM-class:

  package Todo::Worker::GroupMember;
  
  $VERSION=0.1;
  
  use ORM::Base 'Todo::ORM';

We should execute some SQL queries:

  CREATE TABLE `Worker_GroupMember` (
    `id` bigint(20) unsigned NOT NULL auto_increment,
    `group` bigint(20) unsigned NOT NULL default '0',
    `worker` bigint(20) unsigned NOT NULL default '0',
    PRIMARY KEY  (`id`),
    UNIQUE KEY `group_worker` (`group`,`worker`)
  ) TYPE=InnoDB;
  
  INSERT INTO '_ORM_refs' VALUES ( 'Todo::Worker::GroupMember', 'group', 'Todo::Worker::Group' );
  INSERT INTO '_ORM_refs' VALUES ( 'Todo::Worker::GroupMember', 'worker', 'Todo::Worker' );

After this we can access collecions on both sides.

To get members of the $group:

  @workers = Todo::Worker->find
  (
      filter=>
      (
          Todo::Worker->M->_rev( 'Todo::Worker::GroupMember'=>'worker' )->group == $group
      )
  );

or

  @workers = Todo::Worker::GroupMember->stat
  (
      data    => { worker => Todo::Worker::GroupMember->M->worker },
      preload => { worker => 1 },
      filter  => ( Todo::Worker::GroupMember->M->group == $group ),
  );

To get groups the $worker belongs to:

  @groups = Todo::Worker::Group->find
  (
      filter=>
      (
          Todo::Worker::Group->M->_rev( 'Todo::Worker::GroupMember'=>'group' )->worker == $worker
      )
  );

or

  @groups = Todo::Worker::GroupMember->stat
  (
      data    => { group => Todo::Worker::GroupMember->M->group },
      preload => { group => 1 },
      filter  => ( Todo::Worker::GroupMember->M->worker == $worker ),
  );

The advantage of this approach is that you easily can add property to Todo::Worker::GroupMember that for example describes member's position in each group, that is impossible with commonly used 'object collection' properties.

The only drawback of this approach is that linking table should contain 'ID' field. This is to be fixed in the future by allowing object IDs to be multi-field primary keys.

That's it!

LAZY LOAD ^

Lazy loading is a technique in ORMs that attempts to delay fetching of data from database to your program object space until the data is known to be needed.

The implementation of ORM supports per-field lazy load. But interface currently allows only per-table lazy load. Per-field lazy loading is currently used only in cases of updates with server-side expressions (see below).

There are three methods that supports 'lazy_load' option:

First of them is find. Suppose we need to get full list of workers. It can be done with or without lazy load.

  @workers      = Todo::Worker->find;
  @lazy_workers = Todo::Worker->find( lazy_load=>1 );

In first case ORM load all data from Worker table, if there are Todo::Worker::Group objects exist then data from Worker_Group table is loaded and joined.

In second case only Worker table is read and objects are being put in lazy loaded state.

Second method that supports lazy load is find_id.

  $worker = Todo::Worker->find_id( id=>1, lazy_load=>1 );

This means not to load any data from database at all. When you try to access some $worker property ORM attempts to load object from storage, if loading is failed then $worker is reblessed to ORM::Broken.

Also note that while $worker is in lazy loaded state it is assumed to be object of Todo::Worker class even if in fact it is object of Todo::Worker::Group.

To force load of lazy loaded object you can use finish_loading or refresh methods.

Finally stat method accepts 'lazy_load' option that is forwarded to find_id when stat makes decision to load requested objects.

  $result = Todo::Task->stat
  (
      data      => { task=>Todo::Task->M, worker=>Todo::Task->M->worker },
      lazy_load => $lazy_load,
  );

This means to load set of tasks and their related workers. If $lazy_load is true then ORM do not execute separate queries to load complete objects, it just calls to find_id with lazy_load option set to true.

Note that with using stat you can load all objects in one query:

  $result = Todo::Task->stat
  (
      data    => { task=>Todo::Task->M, worker=>Todo::Task->M->worker },
      preload => { task=>1, worker=>1 },
  );

Also note that Objects of Todo::Worker::Group are still not completely loaded (only Worker table is read).

There is corresponding query you can see with ORM::DbLog->write_to_stderr(1).

  --------------------------
  [Sat Jan 21 13:40:57 2006]: ORM::stat: Success
  SELECT
    `_T1_Task`.`worker` AS `_task worker`,
    `_T1_Task`.`title` AS `_task title`,
    `_T1_Task`.`deadline` AS `_task deadline`,
    `_T1_Task`.`id` AS `task`,
    `_T1_Task`.`desc` AS `_task desc`,
    'Todo::Worker' AS `_worker class`,
    `_T1_Task`.`created` AS `_task created`,
    `_T1_Task`.`start_date` AS `_task start_date`,
    `_T2_Worker`.`id` AS `worker`,
    `_T2_Worker`.`name` AS `_worker name`,
    'Todo::Task' AS `_task class`
  FROM
    `Task` AS `_T1_Task`
      LEFT JOIN `Worker` AS `_T2_Worker` ON( `_T1_Task`.`worker`=`_T2_Worker`.`id` )

SERVER-SIDE UPDATES ^

There is useful feature in ORM to do updates using server-side expressions (or meta expressions):

  $cartman->update
  (
      prop=>{ name=>Todo::Worker->M->name->_append( ' (fatboy)' ) }
  );

Meta-expressions are constructed in the same fashion as for find.

After execution of above code $cartman will fall in lazy loaded state. More specifically it will have no idea about new value of name property (Note also that if history is enabled for method or for entire class then the object is being brought to fully loaded state to consider history changes).

This feature is useful to do non-transactional updates in situations when you are not sure about freshness of the object being updated. Without using server-side update you will get error message if your object's data is not up to date.

There is one unsolved issue about server-side updates. For sake of data integrity execution of server-side updates should be permitted only to class itself and to defined class friends. The idea how to make it is still high in the air. This issue is definetely should be solved earlier than version 1.0 of ORM is to be released.

SOME REFACTORING ^

Move property to base class (To be documented)

Move property to child class (To be documented)

Delete property (To be documented)

ORM + APACHE (To be documented) ^

SEE ALSO ^

http://perlorm.sourceforge.net/

ORM

TODO.txt

AUTHOR ^

Alexey V. Akimov

COPYRIGHT AND LICENSE ^

Copyright (C) 2005-2006 Alexey V. Akimov

This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

syntax highlighting: