3

Summary

I want to look into separating the concern of "creating a populated object from database", to use inside my application. DataMapper pattern seems to be doing just that for me.

DataMapper pattern definition: http://martinfowler.com/eaaCatalog/dataMapper.html

However, inside the DataMapper pattern itself there are concerns of

  • object management - creation, population, validation (maybe), return to caller, destruction, saving, updating etc.
  • loading of the data acquired from the database (i.e. construction of SQL, sending SQL to native DB functions, receiving DB data resource, or in PHP's case an associative array of data)
  • mapping of the loaded data from the database into the object's variables (schema-to-object translation, hydration, etc).

My goal as far as my application goes is to start with "I want data, give me data", and DataMapper going "here it is, all ready for ya. And not just the data you wanted, but a whole object, already populated with the data, & mapped properly from the DB schema too!". ("Wow, thanks DataMapper!")

Question:

Can DataMapper pattern contain all of the above concerns inside a single class, or must it split each concern into their own separate classes?

Details & Some Code

Below I will focus on concerns of "data" and "object creation & population". Considering concepts such as separation of concerns, dependency injection, and object oriented programming best practices, is it appropriate for a DataMapper pattern to contain concern of data handling? Can data concern (i.e. SQL) and concern of creating-n-populating-an-object coexist inside one class?

In example below, I have SQL (aka data concern) in my DataMapper class

class MyProductMapper
{
    function getProduct()
    {
        $sql = "SELECT name FROM product limit 1";
        $result = db_query($sql);
        $data = db_fetch_array($result);

        $product = new MyProduct($data);

        return $product;
    }
}  

Should Data Concern be separated?

Consider code below as an alternative to code above (SQL extracted out of DataMapper and moved into its own data class):

//class that concerns itself with receiving some data
//creating an object, and populating that object with the data
//returning that object
class MyProductMapper
{
    function getProduct()
    {
        $data = (new ProductData())->getNameData();  
        $product = new MyProduct($data);

        // return product to the caller
        return $product;
    }
}  

//class concerns itself with data retrieval functionality
class ProductData
{
    function getNameData()
    {
        $sql = "SELECT name FROM product limit 1";
        $result = db_query($sql);
        $dataFromDB = db_fetch_array($result);//array('name'=>"ABC-1234");
        return $dataFromDB;
    }
}  

I have separated the Data retrieval from object creation/object-population. Should data concern be separated from concern of of the DataMapper class, or can it coexist with others inside the same class, or does it matter at all, and one way is just as good as another?

5

I've never seen the point of a class that only queries the database and returns an array (or DataSet or DataTable for those in .NET). The mapping of columns in a database to properties or fields on an object belongs in its own class, not in the constructor. The whole point of a data mapper is to decouple the database schema from the object model. By passing $data in as a constructor argument to MyProduct you have embedded the database schema in your object model.

A more clear and straight forward pattern is to create a repository class that deals exclusively with your object model. It would have a data mapper object that maps columns to fields in your object model.

First, your mapper:

class DataMapper {
    public function toMyProduct($data) {
        $product = new MyProduct();

        foreach ($data as $key => $value) {
            // Simple property injection:
            $product->$key = $value;

            // Or setter injection
            $setter = 'set' . ucfirst($key);
            $product->$setter($value);
        }

        return $product;
    }
}

Then your repository class, which would take the mapper as a constructor argument:

class MyProductRepository {
    private $mapper;

    public function __construct(DataMapper $mapper) {
        $this->mapper = $mapper;
    }

    public function find($id) {
        $sql = "...";
        // build parameterized query
        $result = db_query($sql);
        $data = db_fetch_array($result);//array('name'=>"ABC-1234");

        return $this->mapper->toMyProduct($data);
    }

    public function update(MyProduct $product) {
        // build parameterized query for UPDATE statement
    }
}

The true cleanliness of this approach becomes apparent when you start to use these classes together:

$mapper = new DataMapper();
$products = new MyProductRepository($mapper);
$product = $products->find(10);
echo $product->getName();

You can take it one step further and implement the Unit of Work pattern using a database context object, that wraps all the repository classes for each table in the database:

class DbContext {
    public __construct() {
        $this->mapper = new DataMapper();
        $this->products = new MyProductRepository($this->mapper);
    }

    public $products;

    public function commit() {
        // issue COMMIT command to the database
    }

    public function rollback() {
        // issue ROLLBACK command to the database
    }
}

Now your code becomes even cleaner, with the added benefit of transaction management:

$db = new DbContext();

try {
    $product = $db->products->find(10);
    // do stuff with $product

    $db->products->update($product);
    $db->commit();
} catch (Exception $ex) {
    $db->rollback();

    throw $ex;
}

If you really need a method that returns the raw array from the database, that becomes just another method on your repository class:

class MyProductRepository {
    private $mapper;

    public __construct(DataMapper $mapper) {
        $this->mapper = mapper;
    }

    public function find($id) {
        $data = $this->findData($id);

        return $this->mapper->toMyProduct($data);
    }

    public function findData($id) {
        $sql = "...";
        // build parameterized query
        $result = db_query($sql);
        $data = db_fetch_array($result);//array('name'=>"ABC-1234");

        return $data;
    }

    public function update(MyProduct $product) {
        // build parameterized query for UPDATE statement
    }
}
  • interesting. Thanks! Thoughts that come to mind are as follows. 1) I can put the schema-to-object mapping code inside ProductData class itself. That tightly couples SQL with schema-to-object translation. If schema changes, so does the schema-to-object mapping. This will make ProductData aware of the object structure. I guess it's the reverse of object being aware of the schema .. and is still not desirable, right? – Dennis Jun 30 '15 at 22:27
  • 2) I am looking at blog.tekerson.com/2008/12/17/data-mapper-pattern-in-php It talks of generic container for mapping user objects to DB and back, and seems to be similar. But it has SQL data concern in ::findById(), schema-to-object conversion in ::populate(), and functionality of creating-populating-n-returning object in ::_create(), populate() all in one class... UserMapper. I am evaluating that approach and your approach for best set of practices :) – Dennis Jun 30 '15 at 23:05
  • 1
    I've never seen the point of a class that only queries the database and returns an array (or DataSet or DataTable for those in .NET) -- To encapsulate the SQL and associated database calls. – Robert Harvey Jul 1 '15 at 14:34
  • Just a thought on -- The mapping of columns in a database to properties or fields on an object belongs in its own class, not in the constructor. Suppose there are some classes that I want to initialize via their constructor (i.e. setter vs constructor initialization). Can the Mapper layer be transformed to allow the class to be populated via constructor? – Dennis Jul 1 '15 at 15:17
  • @Dennis: Yes. The mapper could be changed to invoke whichever constructor you want. – Greg Burghardt Jul 1 '15 at 15:55
2

The code sample #2 you provided is preferable in my opinion.

What if tomorrow you want to have CachedProductData, you would only update product data class, or rather introduce another layer.

If you have both classes merged like in example 1, you would be updating both responsibilities.

So, Single Responsibility Principal would be broken if you choose example #1.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.