PCSpectra wrote:My impression was that ORM would typically map 1:1 or 1:m - that is to say - one class may map to one ro more tables. I'm not sure I follow what you mean by n:n.
3 tables - 1 class
For example you could store years, months, & days in 3 different tables respectively; with one class to handle dates (although probably a stupid example on my part)
1 table - 3 classes
Or, you could store the dates as a serialized string in 1 field/table; with separate day, month, & year classes that get separately created & then 'wired' together by injecting into a date object
n - n
Or you could do any crazy combination you wanted. You can even have more than one mapper that can map for a single class, as opposed to a single mapper handling multiple classes or single mapper with multiple tables. SO really maybe its n:n:n not n:n.
Performance
You don't always need all data on all use cases. You can have a mapper highly optimized for each use case, aggressively (as opposed to lazily) loading all data that will be needed on a particular screen, nothing more
Hehe...to be honest I'm confused but thats my fault. If you can find the time, perhaps example code usage of your own shuffler would help clear up any confusion I might have.
Code examples are up here:
http://datashuffler.org/ - but I have provided more realistic examples from my bag of 'real world stuff' below.
For example, instead of using JOINs to build up a user, it's registration date & last login date - you could instead treat 'date' as a 'field' of sorts. For example maybe the process for mapping a date is to get a string format 'MM-DD-YYYY', split it on the comma delimiter, instantiate 3 classes, and wire them together. Essentially instead of having 'date mapper' and your 'user mapper' joining to the dates, the dates are treated as 'field' or mapping. This makes the 'concrete' data mapper code more succinct & decreases the amount of mental work that has to be done to understand the forest from the trees in my opinion.
For example
http://datashuffler.org/ I have 'embedded', 'foreign key single', 'foreign key multiple/assocation', & 'plugin' mappings. Code examples are shown along with descriptions of their mapping strategy. Basically these mappings implement the strategy pattern. I had one application where things had collections of grades. Employers had grades taught. Teachers had grades taught, etc.. Both also had 'subjects taught'
Here is a 'subject mapping' you'll see it uses PHP code to find related objects, instead of SQL, which is possible. (although not required. You could override the select() method or create some coupling between mappings & mappers to cause the data you need to be added to the result set, the result set is then optionally passed to the mapping. This way you can 'mix in' your mappings & still retain 1 query (which only sometimes the fastest way performance wise)
Code: Select all
<?php
/**
* Mapping class for adding subject mapping to an entity
*
* to add this mapping on a mapper ex:
*
* ->addMapping( new Subject_Mapping( $this, array(
* 'field' => 'educator_id',
* 'associationTable' => 'educator_subject'
* )))
*
* @package Subject
* @subpackage Subject_Mapper
*/
class Subject_Mapping extends Shuffler_Mapper_Mapping_Association
{
protected $class = 'Subject';
protected $associationTable;
public function getType()
{
return 'subject';
}
public function getProperty()
{
return 'Subjects';
}
/**
* Perform the select on the association table, load the rows ( subjects ) from that table and
* populate the model with a loaded subject collection
*
* @param Subject_Interface
*/
protected function doLoad( Subject_Interface $model )
{
$db = $this->getReadAdapter();
$select = $db->select()
->from( array( 'assoc' => $this->getAssocTableName() ) )
->where( 'assoc.' . $db->quoteIdentifier( $this->getAssocFk() ) . ' = ?', $model->getId() );
$rs = $this->getReadAdapter()->query( $select )->fetchAll();
$subjects = array();
foreach( $rs as $row )
{
if( $row['subject_level3_id'] )
{
$subject = $this->getMapper( 'Subject_Level3' )->find( $row['subject_level3_id'] );
array_push( $subjects, $subject );
}
else if ( $row['subject_level2_id'] )
{
$subject = $this->getMapper( 'Subject_Level2' )->find( $row['subject_level2_id'] );
array_push( $subjects, $subject );
}
else if ( $row['subject_level1_id'] )
{
$subject = $this->getMapper( 'Subject_Level1' )->find( $row['subject_level1_id'] );
array_push( $subjects, $subject );
}
}
foreach( $subjects as $subject )
{
$model->addSubject( $subject );
}
return $model;
}
/**
* Perform the save function for the model updating the association table for this entity
* with the assigned values from the model
*/
public function save( Shuffler_Model $model )
{
$db = $this->getWriteAdapter();
// delete the association rows
$db->delete( $this->getAssocTableName(), $this->getAssocFk() . ' = ' . (int)$model->getId() );
// and re-insert
$collection = $model->getSubjects();
foreach( $collection as $subject )
{
$values = array();
foreach( $subject->getPath() as $node )
{
$values[ 'subject_level' . $node->getLevel() . '_id' ] = $node->getId();
}
$values[ $this->getAssocFk() ] = $model->getId();
$db->insert( $this->getAssocTableName(), $values );
}
}
}
It is used like this: (control F and highlight all 'addMapping')
Code: Select all
class Educator_Mapper extends Search_Searchable_Mapper
{
/** Set up the field mappings */
function init()
{
$this->addPrimaryKey( 'id' )
->addSingle( 'user', 'User', array(
'immutable' => true
))
->addSingle( 'address', 'Address', array(
'field' => 'address_id',
'property' => 'Address'
))
->addSingle( 'perm_address', 'Address', array(
'field' => 'permanent_address',
'property' => 'PermanentAddress'
))
->addAssociation( 'terms', 'Term', array(
'property' => 'DesiredJobTerm'
))
->addMapping( new Subject_Mapping( $this, array(
'field' => 'educator_id',
'associationTable' => 'educator_subject'
)))
->addMapping( new Application_Mapping( $this ) )
->addAssociation( 'grades', 'Grade' )
->addMapping( new Employer_Type_Mapping( $this, array(
'field' => 'educator_id',
'associationTable' => 'educator_desired_employer_type'
)))
->addMapping( new Geography_Mapping( $this, array(
'field' => 'educator_id',
'associationTable' => 'educator_desired_job_location'
)))
->addEmbedded( 'cover_letter' )
->addEmbedded( 'career_objective' )
->addEmbedded( 'desired_position' )
->addEmbedded( 'last_position' )
->addEmbedded( 'years_experience' )
->addAssociation( 'qualifications', 'Qualification' )
->addSingle( 'education', 'Education', array(
'property' => 'HighestEducation',
'field' => 'highest_education_id'
))
->addAssociation( 'willing_tos', 'Job_Willing', array(
'associationTable' => 'educator_job_willing',
'accessorMethod' => 'getWillingTo',
'mutatorMethod' => 'addWillingTo'
))
->addAssociation( 'desired_neighborhoods', 'Neighborhood', array(
'associationTable' => 'educator_desiredneighborhood',
'property' => 'desiredNeighborhood'
))
->addAssociation( 'documentation', 'Documentation_Level2', array(
'associationTable' => 'educator_documentation',
'accessorMethod' => 'getDocumentation',
'mutatorMethod' => 'addDocumentation'
))
->addAssociation( 'saved_job', 'Job', array(
'associationTable' => 'educator_savedjobs',
'accessorMethod' => 'getSavedJobs',
'mutatorMethod' => 'addSavedJob'
))
->addCollection( 'certification', 'Certification' )
->addCollection( 'application', 'Application' )
->addCollection( 'video', 'Video' )
->addCollection( 'document', 'Document' )
->addEmbedded( 'nbct' )
->addEmbedded( 'housse' )
->addEmbedded( 'hq' );
}