Page 2 of 4
Posted: Thu Dec 21, 2006 10:36 pm
by John Cartwright
Kind of off topic, but I wouldn't consider this very good practice
Code: Select all
public function __construct()
{
if (is_null(self::$_db))
{
if(DNS_Registry::getInstance()->isRegistered('database'))
{
self::setAdapter(DNS_Registry::getInstance()->get('database'));
return;
}
throw new DNS_Db_DataMapper_Exception(
'You must set a database adapter before you can instantiate a mapper'
);
}
}
- I dislike the multiple calls to the register, when a single call can be stored.
- if (is_null(self::$_db)) will always evaluate to true, so why bother?
- My personal preference, and have noticed it is likely the norm, that it is best to first check whether to or everything is setup correctly and then proceed. That way we can avoid ugly returns simply to terminate the method
Code: Select all
public function __construct()
{
$registry = DNS_Registry::getInstance();
if (!$registry->isRegistered('database'))
{
throw new DNS_Db_DataMapper_Exception('You must set a database adapter before you can instantiate a mapper');
}
self::setAdapter($registry->get('database'));
}
Seems much cleaner and easier to read.
Posted: Thu Dec 21, 2006 10:39 pm
by Christopher
I'm a little confused by the code too. I guess the first thing that confuses me is that in the use case they create Model and a Mapper, but in the code the Mapper is an Abstract class and the Model extends it. I would hope the goal would be to have a User class that just had properties and methods to support the application, but that did not need to know how to load and save itself. The Mapper would deal with that.
The thing that stands out is that the Model is full of SQL. According to Fowler "With Data Mapper the in-memory objects needn't know even that there's a database present; they need no SQL interface code, and certainly no knowledge of the database schema. "
Maybe we should start with a User class that knows nothing about any datasource and a User database table. Then we can see how we can create a Mapper that can do the insert, delete, and update while the User class remains blissfully ignorant.
Posted: Thu Dec 21, 2006 10:40 pm
by neophyte
Pssst ... I recognize the example.
Great modification! I like this pattern. It seems like a great way to isolate/encapsulate sql. But I was just wondering what this class might look like if it needed to pull an "INNER JOIN" type of select. How would you deal with complex queries involving more than one table? Or did I miss something?
Posted: Thu Dec 21, 2006 11:03 pm
by Luke
Jcart wrote:if (is_null(self::$_db)) will always evaluate to true, so why bother?
not true...
Code: Select all
<?php
DNS_DataMapper::setAdapter($db);
$mapper = new DNS_DataMapper;
?>
I do like this better though...
Code: Select all
public function __construct()
{
if (is_null(self::$_db))
{
$registry = DNS_Registry::getInstance();
if(!$registry->isRegistered('database'))
{
throw new DNS_Db_DataMapper_Exception(
'You must set a database adapter before you can instantiate a mapper'
);
}
self::setAdapter($registry->get('database'));
}
}
Posted: Thu Dec 21, 2006 11:55 pm
by Christopher
It seems like the direction you are headed is much more like Active Record or Table Data Gateway. Those are simpler solutions than Data Mapper and would get the SQL out of the Model proper and into the inherited gateway class.
Posted: Fri Dec 22, 2006 12:34 am
by Luke
maybe I am... but that's not where I want to be going. I want to understand the data mapper pattern.
Posted: Fri Dec 22, 2006 1:00 am
by Christopher
With a Data Mapper you would have a User class that knows nothing about persisting itself and a Mapper that does. So the question becomes, what does the Mapper need to know to do its job?
Code: Select all
class User {
public $username = '';
public $password = '';
public $active = false;
function setUsername($username ) {
$this->username = $username;
}
function getUsername() {
return$this->username;
}
function setPassword($password) {
$this->password = sha1($password);
}
function checkPassword($password) {
return $this->password == sha1($password);
}
function activate() {
$this->active = true;
}
function inactivate() {
$this->active = false;
}
function isActive() {
return $this->active;
}
}
class User_Mapper extends DNS_Db_DataMapper {
// map properties to db fields and know things like true/false is Y/N in the database
// generate SQL to do insert, delete, and update
// do the loading and saving queries
}
$User = new User;
$User->setUsername('Steve');
$User->setPassword('lollylee');
$User->activate();
$mapper = new User_Mapper;
$mapper->insert($User);
// or
$User = $mapper->load('Steve');
And let's say we have a data table that looks like this:
Code: Select all
id INTEGER
usercode VARCHAR(20)
password VARCHAR(24)
inactive CHAR(1)
Posted: Fri Dec 22, 2006 3:12 am
by Maugrim_The_Reaper
I don't care for the level of automation johno was talking about. I would prefer to have as much control as possible. I do not mind doing things like this:
Both approaches (using public methods, or public getter/setters) can be the same thing if you implement the __get/__set magic methods in PHP5. You can manage preventing non-set public properties by letting __set check whether the property name exists in a private array variable which contains a list of all valid field names with a default NULL value.
This also has the benefit that the magic methods become standard for each Model, and can be refactored to a common parent class - leaving only the field name definition within each subclass.
Posted: Fri Dec 22, 2006 4:23 am
by johno
arborint wrote:I am not sure the distinction of explicit vs implicit given is very accurate either because there are a number of points within such a system that operations can be automatic or manual and for that matter immediate or deferred. I don't think a choice necessarily needs to be made because many of those decisions that you want the application to be able to make at run time -- the Mapper providing several behavior options.
I absolutely agree with you. There is definitely a distinction between them, but I've never written you need to make a decision. Sorry I did not make this clear in a first go.
The Ninja Space Goat wrote:maybe I am... but that's not where I want to be going. I want to understand the data mapper pattern.
Yes, airborint is right, what you showed definitely is a sort of ActiveRecord.
Airborint's example of a data mapper is right and clean, but it is going get messy when it comes to loading of related objects (collections, associations). You probably won't be able to make persistent classes (User class in this case) absolutely unaware of mapper. At least I don't managed to do it without some major design drawbacks.
Persistence is in general recognized as a crosscutting concern (in aspect oriented terminology) so its really hard to modularize. A notoriously known crosscutting concern is logging. It is scattered through multiple layers, polluting and mixing other business logic. When it comes to persistence it is pretty much same.
Posted: Fri Dec 22, 2006 10:13 am
by Luke
OK I see... so you have a model class for each table and then a mapper that knows how to persist that model. That makes sense. So this type of solution seems like it would have to be really unportable, because it would have to be so built in to your particular system... is that right?
Posted: Fri Dec 22, 2006 10:49 am
by neophyte
Isn't true though, that once you are at the point of defining/writing sql that portability is all said and done?
Posted: Fri Dec 22, 2006 10:50 am
by Luke
hmm... maybe? Could you elaborate?
Posted: Fri Dec 22, 2006 11:44 am
by Christopher
I whipped up a quick example that you can put in a .PHP file and run. It only shows very basic mapping and only loading an object by key. But the infrastructure is there to fill out the mapping support and add the insert, update and delete methods.
My idea is to make this core code completely manually configured. Once it is all working then we could add Identity Map and Unit of Work functionality (either plugin or wrapper). Then we could wrapper it in things like creating the mapper classes from an XML definition or by inspecting the class and database.
Code: Select all
<?php
// this is the Data Mapper that map db table name <-> class name and holds the field mappings
// it implements the load(), insert(), update() and delete() Mapper interface
class Db_Datamapper {
var $_data = array(); // debug data for this example
var $class_name = '';
var $table_name = '';
var $mappings = array();
function Db_Datamapper($class_name, $table_name='') {
$this->setClass($class_name);
$this->setTable($table_name);
}
function setClass($class_name) {
$this->class_name = $class_name;
}
function setTable($table_name) {
$this->table_name = $table_name;
}
function setMapping($property_name, $field_name, $type, $size, $key=false, $table_name='', $filters=array()) {
$this->mappings[$property_name] =& new Db_Datamapper_Mapping($property_name, $field_name, $type, $size, $key, $table_name, $filters);
}
function & load($key) {
$row = $this->_data[$key]; // replace this with SQL generation and fetch data
$class_name = $this->class_name;
$object =& new $class_name ();
foreach (array_keys($this->mappings) as $property_name) {
$this->mappings[$property_name]->setObject($object, $row);
}
return $object;
}
function insert() {
}
function update() {
}
function delete() {
}
}
// this maps a db field name <-> class property name
class Db_Datamapper_Mapping {
var $property_name = '';
var $field_name = '';
// none of thes following are used but they show some basic info needed for a mapping
var $type = '';
var $size = 0;
var $key = false;
var $table_name = '';
var $filters=array();
function Db_Datamapper_Mapping($property_name, $field_name, $type, $size, $key=false, $table_name='', $filters=array()) {
$this->property_name = $property_name;
$this->field_name = $field_name;
$this->type = $type;
$this->size = $size;
$this->key = $key;
$this->table_name = $table_name;
$this->filters = $filters;
}
function setObject($object, $row) {
if ($this->property_name && $this->field_name) {
$property = $this->property_name;
if (isset($row[$this->field_name])) {
$object->$property = $row[$this->field_name];
}
}
}
function setRow($row, $object) {
if ($this->property_name && $this->field_name) {
$property = $this->property_name;
if (isset($object->$property)) {
$row[$this->field_name] = $object->$property;
}
}
}
}
// this class maps the "users" data table to "User" class
class User_Mapper extends Db_Datamapper {
function User_Mapper () {
$this->setClass('User');
$this->setTable('users');
$this->setMapping('username', 'userid', 'string', 20, true, '', array());
$this->setMapping('password', 'password', 'string', 24, false, '', array());
$this->setMapping('active', 'inactive', 'boolean', 1, false, '', array());
}
}
class User {
public $username = '';
public $password = '';
public $active = false;
// methods for application
}
$mapper =& new User_Mapper;
// put some test data into the mapper for this example using schema
// id INTEGER
// usercode VARCHAR(20)
// password VARCHAR(24)
// inactive CHAR(1)
$mapper->_data = array(
'Steve' => array(
'id' => 1,
'userid' => 'Steve',
'password' => 'lollypop',
'inactive' => 'N',
),
'Sally' => array(
'id' => 2,
'userid' => 'Sally',
'password' => 'sallybop',
'inactive' => 'N',
),
);
$User = $mapper->load('Steve');
$User2 = $mapper->load('Sally');
echo '<pre>' . print_r($User, 1) . '</pre>';
echo '<pre>' . print_r($User2, 1) . '</pre>';
There are a couple of small things I want to note. One is that this example does not correctly map the database field "inactive" to the class property "active". It should probably use a Filter to convert 'Y' <-> false and 'N' <-> true as it needs to convert from Y/N to true/false and flip the mean from inactive <-> active.
Also the issue of joins and writing back data to multiple tables always comes up. I have added a "table" field to the Mapping that would override the Mapper's defaults -- would have to see how messy that gets.
The internals need to support Identity Map and Unit of Work later so maybe implementing replaceable default support objects that implement these interfaces but are unintelligent might make sense.
Posted: Fri Dec 22, 2006 12:27 pm
by neophyte
Given the messy nature of putting something like this together, Aborint -- Is this sort of thing a viable data solution for PHP? Or does an application that implements this need to implement another strategy for dealing with complex queries (Joins, multiple table updates).
Posted: Fri Dec 22, 2006 12:32 pm
by Luke
don't have time to examine the code fully until lunch, but saw this...
Code: Select all
$mapper->_data = array(
'Steve' => array(
'id' => 1,
'userid' => 'Steve',
'password' => 'lollypop',
'inactive' => 'N',
),
'Sally' => array(
'id' => 2,
'userid' => 'Sally',
'password' => 'sallybop',
'inactive' => 'N',
),
);
lollypop and sallybob... priceless.
