Skip to content
This repository has been archived by the owner on May 26, 2023. It is now read-only.

Commit

Permalink
Add support for eager loading domain objects via include()
Browse files Browse the repository at this point in the history
  • Loading branch information
lox committed Mar 23, 2014
1 parent 62c9caf commit a8ccb85
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 93 deletions.
30 changes: 30 additions & 0 deletions lib/Pheasant/Cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Pheasant;

/**
* A cache for rows that are to be hydrated to objects
*/
interface Cache
{
/**
* @return bool
*/
public function has($hash);

/**
* Gets a row from the cache, or returns false
* @return array
*/
public function get($hash);

/**
* Add or override a row in the cache. Expects a DomainObject
*/
public function add($object);

/**
* Clears the entire cache
*/
public function clear();
}
36 changes: 36 additions & 0 deletions lib/Pheasant/Cache/ArrayCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Pheasant\Cache;

use \Pheasant\Identity;

/**
* An in-memory array backed cache
*/
class ArrayCache implements \Pheasant\Cache
{
private $_cache=array();

public function has($hash)
{
return isset($this->_cache[(string) $hash]);
}

public function get($hash)
{
// hack to avoid an extra isset check
$value = @$this->_cache[(string) $hash];

return $value ? $value : false;
}

public function add($object)
{
$this->_cache[(string) $object->identity()] = $object->toArray();
}

public function clear()
{
$this->_cache = array();
}
}
98 changes: 31 additions & 67 deletions lib/Pheasant/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class Collection implements \IteratorAggregate, \Countable, \ArrayAccess
$_add=false,
$_readonly=false,
$_schema,
$_count
$_count,
$_includes=array()
;

/**
Expand All @@ -26,9 +27,24 @@ public function __construct($class, $query, $add=false)
$this->_query = $query;
$this->_add = $add;
$this->_schema = $schema = $class::schema();
$this->_iterator = new QueryIterator($this->_query, function($row) use ($schema) {
return $schema->hydrate($row);
});
$this->_iterator = new QueryIterator($this->_query, array($this,'hydrate'));
}

/**
* Hydrates a row to a DomainObject
*/
public function hydrate($row)
{
$hydrated = $this->_schema->hydrate($row);

// apply any eager-loaded includes
foreach($this->_includes as $prop=>$includer) {
$hydrated->override($prop, function($prop, $obj) use($includer) {
return $includer->get($obj, $prop);
});
}

return $hydrated;
}

/**
Expand Down Expand Up @@ -264,10 +280,9 @@ public function join($rels, $joinType='inner')
{
$schemaAlias = $this->_schema->alias();

foreach ($this->_normalizeRelationshipArray($rels) as $alias=>$nested) {
$schema = $this->_addJoinForRelationship(
$schemaAlias, $this->_schema, $alias, $nested, $joinType
);
foreach (Relationship::normalizeMap($rels) as $alias=>$nested) {
Relationship::addJoin($this->_queryForWrite(),
$schemaAlias, $this->_schema, $alias, $nested, $joinType);
}

return $this;
Expand All @@ -286,69 +301,18 @@ public function groupBy($columns)
}

/**
* Takes either a flat array of relationships or a nested key=>value array and returns
* it as a nested format
* @return array
*/
private function _normalizeRelationshipArray($array)
{
$nested = array();

foreach ($array as $key=>$value) {
if (is_numeric($key)) {
$nested[$value] = array();
} else {
$nested[$key] = $value;
}
}

return $nested;
}

/**
* Adds a join clause to the internal query for the given schema and relationship. Optionally
* takes a nested list of relationships that will be recursively joined as needed.
* @return void
* Eager load relationships to avoid the N+1 problem
* @chainable
*/
private function _addJoinForRelationship($parentAlias, $schema, $relName, $nested=array(), $joinType='inner')
public function includes($rels)
{
if (!in_array($joinType, array('inner','left','right'))) {
throw new \InvalidArgumentException("Unsupported join type: $joinType");
foreach (Relationship::normalizeMap($rels) as $alias=>$nested) {
$this->_includes[$alias] = new Relationships\Includer(
$this->_query, $this->_schema->relationship($alias)
);
}

list($relName, $alias) = $this->_parseRelName($relName);
$rel = $schema->relationship($relName);

// look up schema and table for both sides of join
$localTable = \Pheasant::instance()->mapperFor($schema->className())->table();
$remoteSchema = \Pheasant::instance()->schema($rel->class);
$remoteTable = \Pheasant::instance()->mapperFor($rel->class)->table();

$joinMethod = $joinType.'Join';
$this->_queryForWrite()->$joinMethod($remoteTable->name()->table, sprintf(
'ON `%s`.`%s`=`%s`.`%s`',
$parentAlias,
$rel->local,
$alias,
$rel->foreign
),
$alias
);

foreach ($this->_normalizeRelationshipArray($nested) as $relName=>$nested) {
$this->_addJoinForRelationship($alias, $remoteSchema, $relName, $nested, $joinType);
}
}

/**
* Parses `RelName r1` as array('RelName', 'r1') or `Relname` as array('RelName','RelName')
* @return array
*/
private function _parseRelName($relName)
{
$parts = explode(' ', $relName, 2);

return isset($parts[1]) ? $parts : array($parts[0], $parts[0]);
return $this;
}

// ----------------------------------
Expand Down
76 changes: 73 additions & 3 deletions lib/Pheasant/Relationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ protected function adder($object)
// -------------------------------------
// delegate double dispatch calls to type

public function getter($key)
public function getter($key, $cache=null)
{
$rel = $this;

return function($object) use ($key, $rel) {
return $rel->get($object, $key);
return function($object) use ($key, $rel, $cache) {
return $rel->get($object, $key, $cache);
};
}

Expand All @@ -85,4 +85,74 @@ public function setter($key)
return $rel->set($object, $key, $value);
};
}

// -------------------------------------
// static helpers

/**
* Takes either a flat array of relationships or a nested key=>value array and returns
* it as a nested format
* @return array
*/
public static function normalizeMap($array)
{
$nested = array();

foreach ((array) $array as $key=>$value) {
if (is_numeric($key)) {
$nested[$value] = array();
} else {
$nested[$key] = $value;
}
}

return $nested;
}

/**
* Adds a join clause to the given query for the given schema and relationship. Optionally
* takes a nested list of relationships that will be recursively joined as needed.
* @return void
*/
public static function addJoin($query, $parentAlias, $schema, $relName, $nested=array(), $joinType='inner')
{
if (!in_array($joinType, array('inner','left','right'))) {
throw new \InvalidArgumentException("Unsupported join type: $joinType");
}

list($relName, $alias) = self::parseRelName($relName);
$rel = $schema->relationship($relName);

// look up schema and table for both sides of join
$instance = \Pheasant::instance();
$localTable = $instance->mapperFor($schema->className())->table();
$remoteSchema = $instance->schema($rel->class);
$remoteTable = $instance->mapperFor($rel->class)->table();

$joinMethod = $joinType.'Join';
$query->$joinMethod($remoteTable->name()->table, sprintf(
'ON `%s`.`%s`=`%s`.`%s`',
$parentAlias,
$rel->local,
$alias,
$rel->foreign
),
$alias
);

foreach (self::normalizeMap($nested) as $relName=>$nested) {
self::addJoin($query, $alias, $remoteSchema, $relName, $nested, $joinType);
}
}

/**
* Parses `RelName r1` as array('RelName', 'r1') or `Relname` as array('RelName','RelName')
* @return array
*/
public static function parseRelName($relName)
{
$parts = explode(' ', $relName, 2);

return isset($parts[1]) ? $parts : array($parts[0], $parts[0]);
}
}
13 changes: 11 additions & 2 deletions lib/Pheasant/Relationships/BelongsTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@
*/
class BelongsTo extends HasOne
{
private $_property;

/* (non-phpdoc)
* @see Relationship::get()
*/
public function get($object, $key)
public function get($object, $key, $cache=null)
{
if(($localValue = $object->{$this->local}) === null)
if ($cache) {
$schema = \Pheasant::instance()->schema($this->class);
if ($cached = $cache->get($schema->hash($object, array($this->local)))) {

This comment has been minimized.

Copy link
@bjornpost

bjornpost Mar 23, 2014

Collaborator

Given Veldwerkdag -- belongsTo (projectID, id) --> Project $schema->hash($object, array($this->local)) results in Model\Project[projectID=x] as where items in the cache are stored as Model\Project[id=x].

At first I thought replacing $this->local with $this->foreign fixed the issue, but I guess this is a bug in Schema::hash() instead.

This comment has been minimized.

Copy link
@lox

lox Mar 24, 2014

Author Owner

I'm struggling to reproduce this, any chance you could put together a test case?

This comment has been minimized.

Copy link
@bjornpost

bjornpost Mar 24, 2014

Collaborator

See #105 . Couldn't reproduce with Hero/SecretIdentity, so I've copied some code from our project. Model and var names are still in Dutch, but I guess you get the picture :-)

return $schema->hydrate($cached);
}
}

if (($localValue = $object->{$this->local}) === null) {
return null;
}

return $this->hydrate($this->query("{$this->foreign}=?", $localValue)
->execute()->row());
Expand Down
2 changes: 1 addition & 1 deletion lib/Pheasant/Relationships/HasMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function __construct($class, $local, $foreign=null)
/* (non-phpdoc)
* @see Relationship::get()
*/
public function get($object, $key)
public function get($object, $key, $cache=null)
{
$query = $this->query(
"{$this->foreign}=?", $object->get($this->local));
Expand Down
11 changes: 9 additions & 2 deletions lib/Pheasant/Relationships/HasOne.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ public function __construct($class, $local, $foreign=null, $allowEmpty=false)
/* (non-phpdoc)
* @see Relationship::get()
*/
public function get($object, $key)
public function get($object, $key, $cache=null)
{
if(($localValue = $object->{$this->local}) === null)
if ($cache) {
$schema = \Pheasant::instance()->schema($this->class);
if ($cached = $cache->get($schema->hash($object, array($this->local)))) {
return $schema->hydrate($cached)->useCache($cache);
}
}

if (($localValue = $object->{$this->local}) === null) {
return null;
}

$result = $this
->query("{$this->foreign}=?", $localValue)
Expand Down
53 changes: 53 additions & 0 deletions lib/Pheasant/Relationships/Includer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Pheasant\Relationships;

use \Pheasant\Query\Criteria;
use \Pheasant\Cache\ArrayCache;

/**
* Finds all possible objects in a relationship that might exist in a query
* and queries them in one shot for future hydration
* @see http://stackoverflow.com/questions/97197/what-is-the-n1-selects-issue
*/
class Includer
{
private
$_query,
$_rel,
$_cache
;

public function __construct($query, $rel)
{
$this->_query = $query;
$this->_rel = $rel;
}

public function loadCache()
{
$this->_cache = new ArrayCache();
$ids = iterator_to_array(
$this->_query->select('DISTINCT '.$this->_rel->local)->execute()->column()
);

$relatedObjects = \Pheasant::instance()
->finderFor($this->_rel->class)
->find($this->_rel->class, new Criteria(
$this->_rel->foreign.'=?', array($ids))
);

foreach ($relatedObjects as $obj) {
$this->_cache->add($obj);
}
}

public function get($object, $key)
{
if(!isset($this->_cache)) {
$this->loadCache();
}

return $this->_rel->get($object, $key, $this->_cache);
}
}
Loading

0 comments on commit a8ccb85

Please sign in to comment.