The Data Mapper

Data Mappers add to the development effort but they give the model the "freedom to be whatever it needs to be". A typical mapper includes:

  • A find($id) method that returns a single Domain object. The method checks the identity map to ensure the object is not already in memory before taking a trip to the database.
  • Other single-object finders, such as findByName($name).
  • Numerous collection finders, such as findAll($status = null) and findByFoo($id).
  • An insert($object) method which constructs an array from the object’s properties then constructs and executes an SQL insert statement.
  • An update($object) method which constructs an array from the object’s updatable properties then constructs and executes an SQL update statement.

In a simple case there may be one Data Mapper for each Domain object however this won’t always be the case. Martin Fowler’s POEAA has a good discussion on the variations, (Chapter 12, Object-Relational Structural Patterns). Patterns that don’t necessarily conform to the 1-to-1 norm include dependent mapping, embedded value, and each of the table inheritance mappers.

As a general rule I start by assuming a 1-to-1 relationship, and when this becomes strained I start looking at the alternatives. Even in cases where it seems obvious to break the 1-to-1 norm, such as class table inheritance, it may transpire that the 1-to-1 works better.

Here is a Bar mapper example.

namespace Mapper;

use Core;

class Bar extends AbstractMapper implements Core\iMapper\Bar
{
    public function find($id)
    {
        if ($exists = $this->getWatcher()->exists($this->getShortType($this), $id)) {
            return $exists;
        }

        $sql    = "SELECT * FROM bar WHERE id = ?";
        $stmt   = $this->getAdapter()->createStatement($sql);
        $result = $stmt->execute(array($id));

        if (!count($result)) {
            return null;
        }

        return $this->create($result->current());
    }

    public function findByFoo($id)
    {
        $sql    = 'SELECT * FROM bar WHERE foo = ?';
        $stmt   = $this->getAdapter()->createStatement($sql);
        $result = $stmt->execute(array($id));
        $rows   = $result->getResource()->fetchAll();

        return new Core\Domain\Collection\Bar(
            $this->getFactory(), $this->convertKeysToCamel($rows));
    }

    public function insert(Core\Domain\iDomain\Bar $bar)
    {
        $data = array(
            ':startDate' => $bar->startDate->format('U'),
            ':condition' => $bar->condition,
            ':foo'       => $bar->foo->id,
            ':baz'       => $bar->baz->id,
        );

        $sql    = 'INSERT INTO bar (startDate, condition, foo, baz) ';
        $sql   .= 'VALUES (:startDate, :condition, :foo, :baz)';
        $stmt   = $this->getAdapter()->createStatement($sql);

        $stmt->execute($data);
        $bar->id = $this->getAdapter()->driver->getLastGeneratedValue();
        $this->getWatcher()->add($bar);

        return $bar->id;
    }

    public function update(Core\Domain\iDomain\Bar $bar)
    {
        $data = array(
            ':id'        => $bar->id,
            ':condition' => $bar->condition,
            ':baz'       => $bar->baz->id,
        );

        $sql    = 'UPDATE bar SET condition = :condition, baz = :baz WHERE id = :id';
        $stmt   = $this->getAdapter()->createStatement($sql);
        $stmt->execute($data);
    }

}

Data Mappers are loaded via an abstract factory, the MapperLoader. Note that the mapper converts field names between under_score format in the RDBMS and camelCase for objects.