CakePHP nie wspiera aktualnie leniwego ładowania obiektów. W prawdzie są jakieś skrypty, ale jeszcze żaden z nich nie chciał mi zadziałać. Zacząłem się zastanawiać: “Gdybym miał zaimplementować Lazy Loading w warstwie modelu, to jak bym to zrobił?” To co mi wyszło korzysta z rozwiązań PHP5, więc może wsparcie gałęzi 1.x Cake’a dla PHP4 implikuje brak tego wzorca?
Tak czy siak, podzielę się z Wami moimi pomysłami.
Zacznijmy od początków. Przeciwieństwem leniwego ładowania jest ładowanie gorliwe (lub zachłanne), to znaczy ładowanie na zapas wszystkiego co może okazać się potrzebne. Warstwa modelu w cake’u mogła by wyglądać tak (duże uproszeczenie, jedyna możliwa relacja to belongsTo).
Eager Loading
class Model { function find(){ $r = new ReflectionClass($this); print "calling ".$r->getName()."::find($conditions)"; $query = "Select * from `".strtolower($r->getName())."s` ". "as `".$r->getName()."`"; var_dump($query); //return query result } function __construct(){ $r = new ReflectionClass($this); print("creating ".$r->getName()."
"); if(isset($this->belongsTo) && is_array($this->belongsTo)){ foreach($this->belongsTo as $related){ $this->{$related} = new $related(); } } } } class User extends Model{ protected $belongsTo = array("Group"); } class Group extends Model{ } $user = new User(); $user->find(); echo "
"; $user->Group->find();
Widzimy, że obiekt Group jest inicjowany długo przed użyciem:
creating User creating Group calling User::find() string 'Select * from `users` as `User`' (length=31) calling Group::find() string 'Select * from `groups` as `Group`' (length=33)
Mało tego, ładuje się nawet jeśli nigdy nie wywołamy metody w tym obiekcie (zakomentuj $user->Group->find(); żeby się o tym przekonać).
Lazy Loading
class Model { function find(){ $r = new ReflectionClass($this); print "calling ".$r->getName()."::find($conditions)"; $query = "Select * from `".strtolower($r->getName())."s` ". "as `".$r->getName()."`"; var_dump($query); //return query result } function __construct(){ $r = new ReflectionClass($this); print("creating ".$r->getName()."
"); } function __get($property){ if(!isset($this->{$property})) { $this->{$property} = new $property(); } if(in_array($property, $this->belongsTo)){ return $this->{$property}; }else{ $r = new ReflectionClass($this); throw new Exception( "$property not related with {$r->getName()}" ); } } } class User extends Model{ protected $belongsTo = array("Group"); } class Group extends Model{ } $user = new User(); $user->find(); echo "
"; $user->Group->find();
W tym rozwiązaniu obiekt Group nie zostanie stworzony, dopóki nie będziemy go potrzebować. Wykorzystana jest do tego magiczna metoda __get, która jest wywoływana wtedy, gdy próbujemy się odnieść do atrybutu obiektu, który nie istnieje.
ale pozostaje kwestia wyszukiwania w modelu tak, żeby zwrócił powiązane dane
np.: var_dump($user->find());
array 0 => array 'User' => array 'id' => int 1 'name' => string 'Franek' (length=6) 'Group' => array 'id' => int 1 'name' => string 'Admin' (length=5) 1 => array 'User' => array 'id' => int 2 'name' => string 'Janek' (length=5) 'Group' => array 'id' => int 10 'name' => string 'User' (length=4)
Wiemy, że belongsTo oznacza, że User powinien mieć pole (group_id), zatem query powinno wyglądać mniej więcej tak:
Select User.id, User.name, Group.id, Group.name from users as User Inner Join groups as Group on (User.group_id = Group.id)
W tej chwili cakephp zawiera behavior containable, jednak on polega na załadowanych już modelach.
Zostańmy przy terminologii cake’a i przyjmijmy, że będziemy przekazywać informacje na temat powiązanych elementów w zmiennej $contain
function find($contain=null)
Teraz chciałbym powiedzieć: obiekcie $user zwróć mi wszystkich użytkowników z bazy wraz z informacją w jakich są grupach:
$user = new User(); $user->find( array( "belongsTo" => array($user->Group) ) );
Zatem zaktualizuję metodę find:
function find($contain=null){ $r = new ReflectionClass($this); print "calling ".$r->getName()."::find($conditions)"; $query = "Select * from `".strtolower($r->getName())."s` ". "as `".$r->getName()."` \n"; if(!empty($contain) && is_array($contain["belongsTo"])){ foreach($contain["belongsTo"] as $belongsToModel){ $query .= $belongsToModel->getBelongsToJoin($this); } } var_dump($query); //return query result }
która od teraz zakłada, że model posiada metodę, która potrafi utworzyć odpowiedniego joina:
function getBelongsToJoin($obj){ $owner_reflection = new ReflectionObject($this); $obj_reflection = new ReflectionObject($obj); return "INNER JOIN `".strtolower($owner_reflection->getName())."s` as `".$owner_reflection->getName()."` ON (`".$owner_reflection->getName()."`.`id` = `".$obj_reflection->getName()."`.`".strtolower($owner_reflection->getName())."_id`) "; }
to jest oczywiście bardzo brzydkie ze względu na refleksje. Metodę getName() można jednak zaimplementować w klasie Model.
Tak czy siak, w tym wypadku nawet w find()’zie uwzględniającym relacje można stosować Lazy Loading
Na koniec całość kodu, z większą ilością relacji i przykładem działania (i małym refactoringiem – skoro już mowa o lazy loading to głupio było ładować piętnaście razy ReflectionClass):
class ModelException extends Exception{} class Model { protected $reflection = null; function getName(){ if(is_null($this->reflection)){ $this->reflection= new ReflectionClass($this); } return $this->reflection->getName(); } function getTableName(){ return strtolower( $this->getName() . "s" ); } function getFKName(){ return strtolower( $this->getName() . "_id"); } function getBelongsToJoin($obj){ $owner_reflection = new ReflectionObject($this); $obj_reflection = new ReflectionObject($obj); return "INNER JOIN `".$this->getTableName()."` as `".$this->getName()."` ON (`".$this->getName()."`.`id` = `".$obj->getName()."`.`".$this->getFKName()."`) "; } function find($contain=null){ $r = new ReflectionClass($this); print "calling ".$r->getName()."::find($conditions)"; $query = "\nSelect * from `".strtolower($r->getName())."s` ". "as `".$r->getName()."` \n"; if(!empty($contain) && is_array($contain["belongsTo"])){ foreach($contain["belongsTo"] as $belongsToModel){ $query .= $belongsToModel->getBelongsToJoin($this); } } var_dump($query); //return query result } function __construct(){ $r = new ReflectionClass($this); print("creating ".$r->getName()."
"); } function __get($property){ if(!isset($this->{$property})) { $this->{$property} = new $property(); } if(in_array($property, $this->belongsTo)){ return $this->{$property}; }else{ $r = new ReflectionClass($this); throw new ModelException( "$property not related with {$r->getName()}" ); } } } class User extends Model{ protected $belongsTo = array("Group", "Unit", "Company"); } class Group extends Model{ } class Unit extends Model{} class Company extends Model{} $user = new User(); $user->find( array( "belongsTo" => array($user->Unit, $user->Company) ) );