Going to implement Data Mapper - looking for resources

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

neophyte wrote: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).
I'll be quite honest that I don't know. I have carefully listened to a lot of discussions about Data Mappers and O/RM over the years. I remember the discussions that lead up to Doctrine and now the ones in Zend Framework. I am a failed Propel user and have used Doctrine once when it was a perfect fit (though it was not that fun). I had never written a Data Mapper until last night when I coded the above.

The real question is can we walk before we run? The problem with O/RM is that it is too sexy! Programmers can't stop trying to create the ultimate Hibernate killer. They all seem to end up being based on the creators philosophy on how to deal with the trade-offs. I say if you have to make a trade-off then stop or make both ways options.

I bet > 90% of the needs PHP programmers could be met with a simple, manually configured system. If you add mix-and-match auto-configuration wrappers/plugins to the various parts then even better. And it should not be difficult to support "Convention Over Configuration" wrappers that read the DB schema and create a class based on user defined conventions.

I don't think supporting joins and multiple table updates will be that hard, but your real question is: Should PHP programmers implement their Models this way? I have said no up until now, as Table Data Gateway satisfies my needs. My question is: Can you create a Data Mapper that is as easy to use? I can see how, for example, with a shopping cart / order system that this would be cleaner than Table Data Gateway. So? Should I continue or is it a waste of time?
(#10850)
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

I think we should definately continue... it is a learning experience more than anything else
wei
Forum Contributor
Posts: 140
Joined: Wed Jul 12, 2006 12:18 am

Post by wei »

I think a data mapper is nearly the most complicated type of object-relational mapping. It should be noted that there are mismatches between the object world and the relational world.

http://blogs.tedneward.com/2006/06/26/T ... ience.aspx

I think that for PHP, a data mapper is in most cases an over kill. One interesting data mapper is the iBatis data mapper, available for ASP.NET, Java and ruby. A PHP version is also available, and it looks like this

Code: Select all

$dsn = 'pgsql:host=localhost;dbname=test'; //Postgres SQL
$conn = new TDbConnection($dsn, 'dbuser','dbpass');
$manager = new TSqlMapManager($conn);
$manager->configureXml('my-sqlmap.xml');
$sqlmap = $manager->getSqlMapGateway();

//a standard object
class User
{
	public $username;
	public $email;
}
add some mapping

Code: Select all

<?xml version="1.0" encoding="utf-8" ?>
<sqlMapConfig>
    <select id="SelectUsers" resultClass="User">
        SELECT username, email FROM users
    </select>
</sqlMapConfig>
then we query for the objects as follows

Code: Select all

$userList = $sqlmap->queryForList("SelectUsers");
 
//Or just one, if that's all you need:
$user = $sqlmap->queryForObject("SelectUsers");
the advantage here inherites from the Data Mapper pattern, is that the User object does not need to know anything about the persistence and the relational database knows nothing about the User object. In additional, with the SqlMap data mapper, you are free to use all the available SQL at your deposal including stored procedures. Data Mappers are suited to mapping existing legacy databases (something that Active Record will be difficult to do). It should be noted that, with a Data Mapper, it is also possible to load Active Records, and then use these Active Records for simple CRUD operations. More details regarding the SqlMap data mapper:
http://xlab6.com/p3/demos/quickstart/?p ... ase.SqlMap

some code and some tests
http://trac.pradosoft.com/prado/browser ... ework/Data
http://trac.pradosoft.com/prado/browser ... nit/SqlMap
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

Ok ... I spent a couple hours and hacked a little more on the Data Mapper I started. It ended up being mostly SQL generation code and that usually points to a big waste of time to me ;). It still only does loading of objects. It supports basic joins but is certainly is not very flexible nor does it handle any error conditions really.

I also threw in the ability to read in the mappings from an XML file as that was mentioned a couple times. That is pretty trivial with Simple XML. I just wanted to see how adding wrappers around the manual configuration core would work. It seems pretty straightforward to add automation.

It is PHP4 code (except SimpleXML) should run PHP4 or PHP5. You should be able to cut and paste into a file to run. The core code (Db_Datamapper, Db_Datamapper_Mapping and Db_Datamapper_Join) is still pretty small -- which is the only promising thing.

Code: Select all

<?php

/*
 * Mock_Db and Mock_Db_Result class are just to support this example
 */
class Mock_Db {
	var $_data = array();
	var $error = 0;

	function Mock_Db() {
// put some test data into the mapper for this example
		$this->_data = array(
			'Steve' => array(
				'id' => 1,
				'userid' => 'Steve',
				'password' => 'lollypop',
				'inactive' => 'N',
				),
			'Sally' => array(
				'id' => 2,
				'userid' => 'Sally',
				'password' => 'sallybop',
				'inactive' => 'N',
				),
			);

	}
	
	function & query($sql) {
	     // get value from "WHERE key='value' at end of SQL
	     $a = split('WHERE', $sql);
	     $a = split('=', $a[1]);
	     $key = trim($a[1], " ='");
	     if (isset($this->_data[$key])) {
	     	$row = $this->_data[$key];
	     } else {
	     	$row = array();
	     	$this->error = 1;
	     }
	     $result = new  Mock_Db_Result($row);
	     return $result;
	}
	
	function isError() {
		return $this->error;
	}
	
}

class  Mock_Db_Result {
	var $_row = array();	// debug data for this example
	var $error = 0;

	function  Mock_Db_Result($row) {
// put some test data into the mapper for this example
		if ($row) {
			$this->_row = $row;
		} else {
			$this->error = 1;
		}
	}
	
	function fetchRow() {
	     return $this->_row;
	}
	
	function isError() {
		return $this->error;
	}
	
}

/*
 * Data Mapper classes start here
 */

class Db_Datamapper {
	var $db = null;
	var $class_name = '';
	var $table_name = '';
	var $objects = array();		// track created objects
	var $mappings = array();
	var $joins = array();

	function Db_Datamapper($db, $class_name, $table_name='') {
	     $this->setDb($db);
	     $this->setClass($class_name);
	     $this->setTable($table_name);
	}
	
	function setDb(&$db) {
	     $this->db =& $db;
	}
	
	function setClass($class_name) {
	     $this->class_name = $class_name;
	}
	
	function setTable($table_name) {
	     $this->table_name = $table_name;
	}
	
	function addMapping(&$mapping) {
		if ($mapping->property_name) {
			// use default table name if none provided
			$mapping->table_name = $mapping->table_name ? $mapping->table_name : $this->table_name;
			$this->mappings[$mapping->property_name] =& $mapping;
			return true;
		}
		return false;
	}

	function addJoin(&$join) {
		$this->joins[] =& $join;
		return true;
	}

	function getTableNames() {
		$tables = array();
		if ($this->table_name) {
			$tables[$this->table_name] = 1;
		}
		foreach (array_keys($this->mappings) as $property_name) {
			if ($this->mappings[$property_name]->table_name) {
				$tables[$this->mappings[$property_name]->table_name] = 1;
			}
		}
		return array_keys($tables);
	}

	function getTableNamesSQL() {
		$tables = $this->getTableNames();
		// more than one table then join
		if (count($tables) > 1) {
			if ($this->joins) {
				$sql = '';
				foreach (array_keys($this->joins) as $id) {
					// are both tables in join in the table array?
					if (in_array($this->joins[$id]->table_name1, $tables) && in_array($this->joins[$id]->table_name2, $tables)) {
						if (! $sql) {
							$sql = $this->joins[$id]->table_name1;
						}
						$sql .= $this->joins[$id]->getSQL();
					}
					return $sql;
				}
			} else {
				return implode(',', $tables);
			}
		} else {
			return $tables[0];
		}
			
	}

	function getFieldNames() {
		$fields = array();
		foreach (array_keys($this->mappings) as $property_name) {
			$fields[$property_name] = $this->mappings[$property_name]->getFieldName();
		}
		return $fields;
	}

	function getKeyField() {
		foreach (array_keys($this->mappings) as $property_name) {
			if ($this->mappings[$property_name]->isKey()) {
				return $this->mappings[$property_name]->getFieldName();
			}
		}
	}

	function getOperationSQL($field_name, $value, $operator='=') {
		if ($field_name && $value && $operator) {
			return "$field_name$operator'$value'";
		}
		return '';
	}

	function & load($key) {
		if (! isset($this->objects[$key])) {
			$fields = $this->getFieldNames();
			$tables = $this->getTableNamesSQL();
			$where = $this->getOperationSQL($this->getKeyField(), $key);
			$row = array();
			if ($fields && $tables && $where) {
				$sql = 'SELECT ' . implode(',', $fields) . ' FROM ' . $tables . ' WHERE ' . $where;
echo "Db_Datamapper::load $key SQL=$sql<br/>";
				$result = $this->db->query($sql);	// replace this with SQL generation and fetch data
				if (! $this->db->isError()) {
					$row = $result->fetchRow();
				}
			}
			
			if ($row) {
				$class_name = $this->class_name;
				$this->objects[$key] =& new $class_name ();
				foreach (array_keys($this->mappings) as $property_name) {
					$this->mappings[$property_name]->setObject($this->objects[$key], $row);
				}
			}
		} else {
echo "Db_Datamapper::load $key already loaded.<br/>";
		}
		return $this->objects[$key];
	}
	
	function insert() {
	}
	
	function update() {
	}
	
	function delete() {
	}
	
}

class Db_Datamapper_Mapping {
	var $property_name = '';
	var $field_name = '';
	var $type = '';
	var $size = 0;
	var $is_key = false;
	var $table_name = '';
	var $filters=array();

	function Db_Datamapper_Mapping($property_name, $field_name, $type, $size, $is_key=false, $table_name='', $filters=array()) {
		$this->property_name = $property_name;
		$this->field_name = $field_name;
		$this->type = $type;
		$this->size = $size;
		$this->is_key = $is_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;
	     	}
	     }
	}

	function getFieldName() {
	     if ($this->table_name) {
	     	return $this->table_name . '.' . $this->field_name;
	     } else {
	     	return $this->field_name;
	     }
	}

	function isKey() {
	     return $this->is_key;
	}

}

class Db_Datamapper_Join {
	var $table_name1;
	var $field_name1;
	var $table_name2;
	var $field_name2;
	var $join_type;

	function Db_Datamapper_Join($table_name1, $field_name1, $table_name2, $field_name2, $join_type='') {
		$this->table_name1 = $table_name1;
		$this->field_name1 = $field_name1;
		$this->table_name2 = $table_name2;
		$this->field_name2 = $field_name2;
		$this->join_type = $join_type;
	}

	function getSQL() {
		return " {$this->join_type} JOIN {$this->table_name2} ON {$this->table_name1}.{$this->field_name1}={$this->table_name2}.{$this->field_name2}";
	}

}

class Db_Datamapper_Xml extends Db_Datamapper {

	function Db_Datamapper_Xml (&$db, $filename) {
		$xml =& simplexml_load_file($filename);
		if ($xml) {
			$this->setDb($db);
			$this->setClass(strval($xml->class));
			$this->setTable(strval($xml->table));
			foreach ($xml->mapping as $map) {
				$property = strval($map->property);
				$field = strval($map->field);
				$type = strval($map->type);
				$size = strval($map->size);
				$is_key = strval($map->is_key);
				$table = strval($map->table);
				$filters = strval($map->filters);
				$this->addMapping(new Db_Datamapper_Mapping($property, $field, $type, $size, $is_key, $table, $filters));
			}
			foreach ($xml->join as $join) {
				$table1 = strval($join->table1);
				$field1 = strval($join->field1);
				$table2 = strval($join->table2);
				$field2 = strval($join->field2);
				$join_type = strval($join->join_type);
				$this->addJoin(new Db_Datamapper_Join($table1, $field1, $table2, $field2, $join_type));
			}
		}
	}
	
}

/*
 * Example code start here
 */

class User_Mapper extends Db_Datamapper {

	function User_Mapper (&$db) {
		$this->setDb($db);
		$this->setClass('User');
		$this->setTable('users');
		$this->addMapping(new Db_Datamapper_Mapping('username', 'userid', 'string', 20, true, '', array()));
		$this->addMapping(new Db_Datamapper_Mapping('password', 'password', 'string', 24, false, '', array()));
		$this->addMapping(new Db_Datamapper_Mapping('active', 'inactive', 'string', 1, false, '', array()));
// uncomment these two lines and comment previous line to show join generation
#		$this->addMapping(new Db_Datamapper_Mapping('active', 'inactive', 'string', 1, false, 'company', array()));
#		$this->addJoin(new Db_Datamapper_Join('users', 'userid', 'company', 'userid', 'LEFT'));
	}
}

class User {
	var $username = '';
	var $password = '';
	var $active = false;
}


$mapper =& new User_Mapper(new Mock_Db());
#$mapper =& new Db_Datamapper_Xml(new Mock_Db(), 'mapping01.xml');

$User1 = $mapper->load('Steve');
$User2 = $mapper->load('Sally');

echo 'User1:<pre>' . print_r($User1, 1) . '</pre>';
echo 'User2:<pre>' . print_r($User2, 1) . '</pre>';

// loading a record again should return the same object loaded above without loading
$User3 = $mapper->load('Steve');
And here is the XML file if anyone cares...

Code: Select all

<?xml version="1.0" encoding="utf-8" ?>
<map>
	<class>User</class>

	<table>users</table>

	<mapping>
		<property>username</property>
		<field>userid</field>
		<type>string</type>
		<size>20</size>
		<is_key>1</is_key>
		<table></table>
		<filters></filters>
	</mapping>

	<mapping>
		<property>password</property>
		<field>password</field>
		<type>string</type>
		<size>24</size>
		<is_key></is_key>
		<table></table>
		<filters></filters>
	</mapping>

	<mapping>
		<property>active</property>
		<field>inactive</field>
		<type>string</type>
		<size>20</size>
		<is_key></is_key>
		<table>company</table>
		<filters></filters>
	</mapping>

	<join>
		<table1>users</table1>
		<field1>userid</field1>
		<table2>company</table2>
		<field2>userid</field2>
		<join_type>LEFT</join_type>
	</join>
</map>
edited to run in PHP4, XML only works in PHP5
Last edited by Christopher on Sat Dec 23, 2006 3:43 pm, edited 1 time in total.
(#10850)
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

very nice... I don't have time to play around with that right now, but either tonight or tomorrow night, I will run that code and see if I can understand how everything works... thank you so much arborint--you are always so helpful (in a "teach a man to fish" and not "give a man a fish" kind of way). You're awesome!
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

Hopefully someone will have improvements to this hack. The idea is really pretty simple. The mapper contains the associated class name and table name, and a list of field mappings each containing associated property name and field name. That gives the information to go back and forth from the database to the object.

The next thing that needs to be implemented is an Identity Map which would probably just be an array containing a copy of the values in the object properties. If any of the values has changed then calling update() will write back the changed values.

Everything else is to support SQL generation which is just messy. It would probably be better to move the SQL stuff to a separate class so there could be one for each DB.
(#10850)
User avatar
johno
Forum Commoner
Posts: 36
Joined: Fri May 05, 2006 6:54 am
Location: Bratislava/Slovakia
Contact:

Post by johno »

ARBORINT: You should have definitely looked at http://torpedeo.googlecode.com/svn/trunk/core/ before spending those couple of hours. Basically Torpedeo does same with less and IMO cleaner code. But I must admit that self experience is always the best one.

Hey and don't forget about Money class. One property/field can be mapped to multiple rows and vice versa. I've found a nice clean way to handle this one. Look for mapTransferToRow and mapRowToTransfer methods on ClassMapping and FieldMapping classes.
arborint wrote:The next thing that needs to be implemented is an Identity Map which would probably just be an array containing a copy of the values in the object properties. If any of the values has changed then calling update() will write back the changed values.
Watch out, that's not an Identity Map. At least not how Fowler defines it. http://www.martinfowler.com/eaaCatalog/identityMap.html

And yes, moving SQL stuff to separate select, insert, update, delete classes is a good choice. Look how lastcraft made it in his Changes library. Definitely worth a look.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

johno wrote:ARBORINT: You should have definitely looked at http://torpedeo.googlecode.com/svn/trunk/core/ before spending those couple of hours. Basically Torpedeo does same with less and IMO cleaner code. But I must admit that self experience is always the best one.
I took a quick look, very nice and certainly cleaner, but not less yet and PHP5 only. ;) They are surprisingly similar even in method names.
johno wrote:Hey and don't forget about Money class. One property/field can be mapped to multiple rows and vice versa. I've found a nice clean way to handle this one. Look for mapTransferToRow and mapRowToTransfer methods on ClassMapping and FieldMapping classes.
That sounds like something rare that should be handled by an add-on.
johno wrote:Watch out, that's not an Identity Map. At least not how Fowler defines it. http://www.martinfowler.com/eaaCatalog/identityMap.html
You are right, Identity Map is already implemented ... I meant Unit of Work.
johno wrote:And yes, moving SQL stuff to separate select, insert, update, delete classes is a good choice. Look how lastcraft made it in his Changes library. Definitely worth a look.
I have and remember it well. Not exactly a Data Mapper though ... probably better.
(#10850)
wei
Forum Contributor
Posts: 140
Joined: Wed Jul 12, 2006 12:18 am

Post by wei »

a quick question: why is SQL generation often suggested? Are people afraid of SQL? SQL generation may work for simple cases and for one database. Something like the MySQL's "LIMIT x,y" is very different for other database, e.g. postgres, is something like, "LIMIT x OFFSET y", oracle has no such thing.

Then there are joins, including outer left, outer right, and various other things such as Union. I think a data mapper should be able to support these things, as it should only provide a mapping between what the database returns and how it maps to objects, nothing should be done about SQL. The data mapper should not even depedend on a particular table, just on the actual data returned.

Wei.
User avatar
johno
Forum Commoner
Posts: 36
Joined: Fri May 05, 2006 6:54 am
Location: Bratislava/Slovakia
Contact:

Post by johno »

wei wrote:a quick question: why is SQL generation often suggested? Are people afraid of SQL? SQL generation may work for simple cases and for one database. Something like the MySQL's "LIMIT x,y" is very different for other database, e.g. postgres, is something like, "LIMIT x OFFSET y", oracle has no such thing.

Then there are joins, including outer left, outer right, and various other things such as Union. I think a data mapper should be able to support these things, as it should only provide a mapping between what the database returns and how it maps to objects, nothing should be done about SQL. The data mapper should not even depedend on a particular table, just on the actual data returned.

Wei.
WEI: Those are all special cases in point of a data mapper. A data mapper should be able to generate simple (create, update, insert, delete) statements for you to save you some typing. Once it comes to unions, joins, subselects and so on its really better to go through Criteria based approach, then OQL or raw SQL at last.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I agree with you comments and hopefully can come up with a way to be able to plugin any SQL desired. I may keep coding if I get time, there is not much else needed for a basic Data Mapper. But just to be clear, my goal here is not to reimplement some super O/RM system (there are plenty of those) my goals here are:

- Learn about and demonstrate a Data Mapper

- Show some code that is easy for people to run

- Only try to meet the 80/20 rule with standard features, so very basic SQL
(#10850)
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

johno wrote:Once it comes to unions, joins, subselects and so on its really better to go through Criteria based approach, then OQL or raw SQL at last.
Can you give us some examples of PHP implementations of Criteria based approaches. It is sort of what I am doing with the Db_Datamapper_Join class and addJoin() method. Does it make sense to have a generic addCriteria() method and everything is typed, or to support each SQL functionality separately? Or is it better just to allow people to add custom SQL.
(#10850)
wei
Forum Contributor
Posts: 140
Joined: Wed Jul 12, 2006 12:18 am

Post by wei »

I agree with simple CRUD queries, however,
Once it comes to unions, joins, subselects and so on its really better to go through Criteria based approach, then OQL or raw SQL at last.
this is where I think it is not useful, complicated, and unintutiative to provide any thing other than just SQL. Most interesting and useful queries do indeed require some sort of joins, and aggregate functions. A useful feature may be to move the actual SQL statements away from PHP code. Adding an "OQL" increase complexity a great deal and hinders the abillity to provide support for more database (if that is one of the goals). A criteria object may be useful in some cases, again it also adds complexity.

I think this relates to "leaky abstractions" http://www.joelonsoftware.com/articles/ ... tions.html
the more you try to abstract away, the more holes there are and more complexity. [/quote]
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I have the same feeling that simply allowing custom SQL to be used is probably better than some "OQL" thingy.
(#10850)
User avatar
johno
Forum Commoner
Posts: 36
Joined: Fri May 05, 2006 6:54 am
Location: Bratislava/Slovakia
Contact:

Post by johno »

Ok, guys. First of all, you don't need to choose one approach (Criteria/OQL/SQL) and go for it. You can have them all if you want and choose the most appropriate one for each case you will encounter. At least I would do it so and that is what I wanted to say in first place.
arborint wrote:Can you give us some examples of PHP implementations of Criteria based approaches. It is sort of what I am doing with the Db_Datamapper_Join class and addJoin() method. Does it make sense to have a generic addCriteria() method and everything is typed, or to support each SQL functionality separately? Or is it better just to allow people to add custom SQL.
http://propel.phpdb.org/docs/user_guide ... s.Criteria But I personally never liked that API. A really good resource for some API comparison is http://wiki.cc/php/Object_Relational_Mapping
wei wrote:this is where I think it is not useful, complicated, and unintutiative to provide any thing other than just SQL.
OQL is very similar to SQL. Actually I believe it was designed to overcome some things you have to type manually in SQL and could be automated. So it makes sense it this direction. Look at Hibernate HQL and EZOQL.
wei wrote:Most interesting and useful queries do indeed require some sort of joins, and aggregate functions. A useful feature may be to move the actual SQL statements away from PHP code. Adding an "OQL" increase complexity a great deal and hinders the abillity to provide support for more database (if that is one of the goals). A criteria object may be useful in some cases, again it also adds complexity.
Yes, I absolutely agree with that first sentence. But OQL or at least HQL and EZOQL can handle joins, aggregates, ... because OQL it just SQL with a little sugar on top. If you know SQL you will learn OQL almost instantly.

Ok, now my personal views. I don't like Criteria based approaches because mostly writing raw SQL is just a faster way of doing it for me. Except the really trivial ones (CRUD + trivial SELECTs). On the other hand with raw SQL you need to know how your OR mapper maps classes to tables and properties to columns. So you are loosing abstraction of knowing nothing about persistence internals. OQL is just a middle man and I personally never used it. Not because I don't wanted, but I just never used a solution that supported it.

I prefer writing raw SQL, but my view may be biased because I know SQL. I know people that have big problems using joins and understanding relational domain. Full blown (bloated?) criteria based approaches are I think mostly for this kind of people.

A new problem with raw SQL is that you have to define how the result maps back to objects. With simple queries its easy. But who writes raw SQL for simple ones? I've written about this issue at Sitepoint and proposed a SQL enhancement for this. Never managed to actually implement it, but I really like the core idea.

So definitelly go for raw SQL and at this stage no OQL.
Post Reply