Here’s the difference between the 2 patterns.
Data Mapper gives you a seam for where you can change the way the loading strategy works
Active Record would give you an “all or nothing” approach (If I change the load() method on a class it changes the way that class is loaded EVERYWHERE).
With data mapper I can use different strategies for different scenarios.
====================================================
Both patterns may implement lazy loading. You would only instantiate the mapper for the “root” object and could then traverse it’s related objects. This root object’s mapper gives you the encapsulation that you’d want to be able to go back and change the way the mapping for it’s component objects are wired up. Therefore a fluent “Active Record-ish” interface to it’s component objects is not a bad thing. Even though it seems like using an Active Record it is not because the root object’s mapper may decide to force a new loading strategy at any time.
Fowler outlines multiple lazy load patterns. The one my data mapper implementation uses is the one where I wrap a null object in a data aware “proxy wrapper”. When you call any of the methods on that wrapper it goes behind the scenes, finds the right mapper and calls the finder on it.
Now heres what you CANT do with Active Record. With Active Record you can’t go back and change that so it’s a join. Lets say you had your nice lazy loading traversal, right… what do you do when that slows to a halt from rippling?
While both patterns allow you to use a JOIN instead of using the lazy loading, it is more natural with data mapper (thats just my opinion).
See following for example:
<?php
class Shuffler_Collection_Lazy extends Shuffler_Collection
{
/** @var Shuffler_Mapper_Mapping_Collection */
protected $mapping;
/** @var Shuffler_Model */
protected $model;
/** @var bool wether this lazycollection has been loaded ( initialized ) */
protected $loaded = false;
/** @var count of records */
protected $count;
/** @var integer */
protected $added_before_load;
protected $db_count;
/**
* @param Shuffler_Mapper_Mapping_Collection
* @param Shuffler_Model
*/
public function __construct( Shuffler_Mapper_Mapping_Collection $mapping, Shuffler_Model $model )
{
parent::__construct( array(), $mapping->getClass() );
$this->mapping = $mapping;
$this->model = $model;
}
/**
* Adds a certain model from the collection
* @param Shuffler_Model to add
*/
public function addModel( Shuffler_Model $model )
{
if( !$this->loaded && !$this->hasModel( $model ) )
{
$this->added_before_load++;
}
return parent::addModel( $model );
}
/** @return ArrayIterator */
public function getModels()
{
$this->load();
return parent::getModels();
}
/** @return bool */
public function contains( $compare )
{
$this->load();
return parent::contains( $compare );
}
/**
* Get the model at a specific index in the results array
*
* @param integer index
*
* @return Shuffler_Model
*/
public function getModelAtIndex( $index )
{
$this->load();
return parent::getModelAtIndex( $index );
}
/**
* @return Shuffler_Collection
*/
public function getModel()
{
return $this->model;
}
/**
* Removes a certain model from the collection
* @param Shuffler_Model to remove
*/
public function removeModel( Shuffler_Model $model )
{
$this->load();
return parent::removeModel( $model );
}
/**
* Get # of models this collection currently has
*
* @return int count
*/
public function count()
{
if( false === $this->loaded && isset( $this->count ) )
{
return $this->added_before_load + $this->count;
}
if( false === $this->loaded )
{
return $this->added_before_load + $this->getDbCount();
}
else
{
$this->count = parent::count();
}
return $this->count;
}
public function load()
{
unset($this->count);
if( false === $this->loaded )
{
$this->loaded = true;
$this->merge( $this->mapping->loadCollection( $this->getModel() ) );
}
}
protected function getDbCount()
{
if( !isset( $this->db_count ) )
{
$this->db_count = $this->mapping->countCollection( $this->getModel() );
}
return $this->db_count;
}
}
This class just wraps my normal collections. Instead of loading all the data when a collection is needed, this “lazy” one is injected instead. When you try to call any of its methods it triggers the loading with $this->load() which loads the models in that collection.
A more efficient way, like you said would be to load only $orders[0] instead of the whole $orders array. At least this way you aren’t loading every order’s item, and each of those item’s customers, and all those customers orders, and all those order’s items, and all those items customers, ad infintum
The “count” logic is a little complex because one of it’s features is that I can add models to it before it has done its lazy loading, then later when It does its lazy loading it takes into account both models that I added before load, and the models it pulled from the database post load.
Of course if it ever did become a problem I could override specific finders as needed, to aggressively JOIN instead of lazily inject these ghost collections.
If your active record framework uses “finders” and supports these features, you should ask yourself if the framework’s maintainers know what patterns they are using Nothing wrong with a hybrid approach but there are some Apples out there pretending to be Oranges if you get what I mean, and vice versa