Lazy Loading w modelach a’la CakePHP

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) ) );
Share Button

4 thoughts on “Lazy Loading w modelach a’la CakePHP

  1. patrzac na dzialanie contain (vel unbind/bind) samo wywolanie niepotrzebnych klas jest mniej meczace niz koniecznosc pozniejszych scalen w tablice tych niepotrzebnych danych…

  2. @red: dzięki, moim celem nie było znalezienie narzędzia, tylko próba samodzielnego rozwiązania problemu. Ale dzięki za link – chętnie rzucę okiem (choć mam wrażenie, że już raz na ten plugin wpadłem, zdaje się, że wtedy nie chciał mi działać)
    @AgBorkowski: Lazy Loading mógłby rozwiązać nie tylko problem wydajności. Testując modele mam dość spore problemy z zarządzaniem Fixture’ami. Cake ładuje wszystkie modele w relacji, przez co domaga się istnienia wszystkich tabel dla tych danych, nawet jeśli w tych testach są nieistotne.

    No i na koniec – chciałem przedstawić inny pomysł niż contain, któremy przekazujemy łańcuchy z informacją. Wolałem przekazać obiekt. To nie jest propozycja na rewolucję w cake’u, bo prostu własny “research” ;)

  3. na szczescie gdy wyjdzie Cake 2.0 nie bedziemy musieli zajmowac sie takimi oczywistosciami jak lazy loading modeli :)

Leave a Reply

Your email address will not be published. Required fields are marked *