Login with OpenID

Building an Identity Map in PHP

Written by Hector Virgen
Published on February 9, 2010
Last updated on February 9, 2010

I have been using an identity map in my web applications for quite some time. It's useful to keep track of all of the domain entities that have been created throughout the life of the request. It keeps me from loading the same object twice and makes it easier to do strict comparisons (===).

Without an identity map, you can easily run into problems because you may have more than one object that references the same domain entity.

$userA = $userMapper->find(123); // new object created
$userB = $userMapper->find(123); // new object created

echo $userA->getName(); // Hector
echo $userB->getName(); // Hector

$userA->setName('Bob');
echo $userA->getName(); // Bob
echo $userB->getName(); // Hector ?!?

The identity map solves this problem by acting as a registry for all loaded domain instances.

To build my identity map, the first thing I did was to make sure that each of my entities had a unique ID to distinguish it from other entities of the same type. To enforce this, I created an interface for my entities to implement:

interface Virgen_Entity_Interface
{
    public function getId();
}

The next step was to update my domain entities to implement this interface.

class Default_Model_User implements Virgen_Entity_Interface
{
    protected $_id;

    public function setId($id)
    {
        $this->_id = (int) $id;
    }

    public function getId()
    {
        return $this->_id;
    }
}

The ID can be anything, but because I'm using a relational database with an auto-incrementing primary key, I can use that value as my ID because I know it will be unique for each user.

Next comes the Identity Map. It will act as a registry to store entities so I can load it again later. To ensure that we don't end up with duplicates, we'll use a static array property to store the entities:

class Virgen_Entity_IdentityMap
{
    protected static $_identities = array();

    public static function loadEntity($class, $id)
    {
        $key = self::_generateKey($class, $id);
        if (isset(self::$_identities[$key])) {
            return self::$_identities[$key];
        }
        return false;
    }

    public static function storeEntity(Virgen_Entity_Interface $entity)
    {
        $class = get_class($entity);
        $id = $entity->getId();
        $key = self::_generateKey($class, $id);
        self::$_identities[$key] = $entity;
    }

    protected static function _generateKey($class, $id)
    {
        return $class . '-' . $id;
    }
}

The identity map has two public static functions that you can use to load and store entities.

You'll notice that the loadEntity() method accepts a class name and an ID. The reason for this is you don't want to have to instantiate the object before checking if you need to instantiate it. By passing in the class name and ID, you can query the identity map for the entity. If it returns an object, then you're good to go. If it's not already loaded, the identity map will return false and you'll need to create your object manually.

The storeEntity() method accepts an instance of Virgen_Entity_Interface, and calls its getId() method to help it create a unique key for the domain entity. The unique key is the entity's class name joined to the ID with a hyphen.

The IdentityMap works best with data mappers. Here's an example data mapper for the "user" entity.

class Default_Model_Mapper_User
{
    public function find($id)
    {
        if (false !== ($user = Virgen_Entity_IdentityMap::loadEntity('Default_Model_User', $id))) {
            // Found existing user, return it.
            return $user;
        }
        
        // Existing user not loaded
        $user = new Default_Model_User();
        
        // Populate $user with data from persistant storage
        /* ... */

        // Store user in identity map
        Virgen_Entity_IdentityMap::storeEntity($user);

        return $user;
    }
}

The first time you try to find the user ID 123, the identity map will return false and the mapper will do it's work of constructing it from the data in the persistant storage. But the second time you call find with that same ID, the identity map will return the object created earlier.

$mapper = new Default_Model_Mapper_User();

$userA = $mapper->find(123); // new object created
$userB = $mapper->find(123); // same object return

echo $userA->getName(); // Hector
echo $userB->getName(); // Hector

$userA->setName('Bob');

echo $userA->getName(); // Bob
echo $userB->getName(); // Bob

Much better :)

If your data mapper is also responsible for saving domain entities in the persistant storage, you will also want to update your save() method to store the entity in the identity map after a successful save. This way, you can be sure that if you want to find that entity again later it will be pulled from the identity map:

class Default_Model_Mapper_User
{
    /* ... */

    public function save(Default_Model_User $user)
    {
        if (null !== ($id = $user->getId())) {
            // update persistant storage
        } else {
            // insert into persistant storage and capture ID
        }

        Virgen_Entity_IdentityMap::storeEntity($user);
    }
}

By using an identity map you can be confident that your domain entity is shared throughout your application for the duration of the request.

Note that using an identity map is not the same as adding a cache layer to your mappers. Although caching is useful and encouraged, it can still produce duplicate objects for the same domain entity.

However, caching can further improve your data mappers but its important to note the order:

  1. Check the identity map first.
  2. If the entity is not found, check the cache.
  3. If not in the cache, load it from persistant storage.

By following the order above you can reap the benefits of both identity maps and caching systems.

Comments

blog comments powered by Disqus