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:
* the same as of "ORM::_property" for plain (non-object) properties
* database property value for non-ORM third party classes
* object's ID for ORM classes
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 );
}
* Method "_validate_prop" is implicitly called when new object is being
created ("new" method) and when object is being updated ("update"
method).
* Condition "( $arg{method} eq 'new' )" is true only when called from
within "new" method. In another words this means that object is not yet
stored in database table.
* Method "ORM::_fix_prop" is intended to use only within
"_validate_prop".
* Do not forget to call "SUPER::_validate_prop".
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 planned to be done by specific worker
* Tasks that should be done due specified date
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