Доброго времени суток, хабрасообщество!
При работе с базой данных или просто с объектами, доступными из разных частей вашего приложения, есть опасность, что объекты, которые, казалось бы, должны быть равны вовсе таковыми не являются.
Например, допустим, у нас есть некоторая модель ActiveRecord — Expence и вот такой код:
Таким образом, меняя одну модель мы никоим образом не затронем вторую(что логично, так как они ссылаются на разные объекты).
Для решения данной проблемы воспользуемся шаблоном проектирования, названным Мартином Фаулером Identity Map.
Его идея заключается в том, чтобы отслеживать наличие в приложении объектов имеющий один и тот же идентификатор. Таким образом, если мы запрашиваем модель с id равным 5 в двух разных местах программы, мы получим ссылку на один и тот же объект.
К сожалению, Yii не следит за тем, запрашивали ли мы какой либо объект из базы или нет, поэтому придется написать свой собственный класс.
С помощью методов addRecord и getRecord мы добавляем и получаем модель из своеобразного реестра моделей(в качестве которого выступает ассоциативный массив вида «имякласса.ид» => объект).
Теперь необходимо заставить Yii создавать объект, если он еще не был получен, или возвращать имеющийся из массива $objects. Будем пытаться сделать это средствами самого Yii, не создавая лишних прослоек после CActiveRecord. Конечно, хотелось бы после при выполнении Expence::model()->findByPk(10) выдавать полученный ранее объект без запроса к базе, но в Yii нет механизма перехвата данного запроса. Да, есть CActiveRecord::beforeFind(), но из него невозможно получить данные о запросе, в частности после findByPk(). Экземпляр модели создается в методе CActiveRecord::instantiate(). Переопределим его в нашей модели.
Вот и все, теперь
Ссылки:
Проблема
При работе с базой данных или просто с объектами, доступными из разных частей вашего приложения, есть опасность, что объекты, которые, казалось бы, должны быть равны вовсе таковыми не являются.
Например, допустим, у нас есть некоторая модель ActiveRecord — Expence и вот такой код:
$modelOne = Expence::model()->findByPk(10);
$modelTwo = Expence::model()->findByPk(10);
var_dump($modelOne === $modelTwo); // Вернет false
Таким образом, меняя одну модель мы никоим образом не затронем вторую(что логично, так как они ссылаются на разные объекты).
$modelOne->someField = "Data";
$modelOne->save();
/// ...какой-то код...
echo $modelTwo->someField; // Содержит старое значение
$modelTwo->save(); // Затираем ранее записаные данные
Решение
Для решения данной проблемы воспользуемся шаблоном проектирования, названным Мартином Фаулером Identity Map.
Его идея заключается в том, чтобы отслеживать наличие в приложении объектов имеющий один и тот же идентификатор. Таким образом, если мы запрашиваем модель с id равным 5 в двух разных местах программы, мы получим ссылку на один и тот же объект.
Реализация
К сожалению, Yii не следит за тем, запрашивали ли мы какой либо объект из базы или нет, поэтому придется написать свой собственный класс.
<?php
/**
* Singleton class to manipulate instances of models (e.g. CActiveRecord).
*
* @author Yuriy Ratanov <organium@gmail.com>
*/
class ObjectWatcher {
/**
* Current instance of ObjectWatcher
* @var ObjectWatcher
*/
private static $_instance;
/**
* Array of objects to work with.
* @var array
*/
private $objects = array();
/**
* Geting instance of ObjectWatcher.
* @return ObjectWatcher
*/
static function getInstance(){
if(!isset(self::$_instance)){
self::$_instance = new ObjectWatcher;
}
return self::$_instance;
}
/**
* Getting instance of the object existing in the current application.
* @param string $className
* @param int $id
* @return mixed null or object of the class $className with an id = $id if it exists.
*/
static function getRecord($className, $id) {
$inst = self::getInstance();
$key = "$className.$id";
if(isset($inst->objects[$key])){
return $inst->objects[$key];
}
return null;
}
/**
* Adding object to ObjectWatcher registry.
* @param $obj
* @param int $id
*/
static function addRecord($obj, $id) {
$inst = self::getInstance();
$inst->objects[$inst->getKey($obj, $id)] = $obj;
}
function getKey($obj, $id){
return get_class($obj).'.'.$id;
}
}
С помощью методов addRecord и getRecord мы добавляем и получаем модель из своеобразного реестра моделей(в качестве которого выступает ассоциативный массив вида «имякласса.ид» => объект).
Теперь необходимо заставить Yii создавать объект, если он еще не был получен, или возвращать имеющийся из массива $objects. Будем пытаться сделать это средствами самого Yii, не создавая лишних прослоек после CActiveRecord. Конечно, хотелось бы после при выполнении Expence::model()->findByPk(10) выдавать полученный ранее объект без запроса к базе, но в Yii нет механизма перехвата данного запроса. Да, есть CActiveRecord::beforeFind(), но из него невозможно получить данные о запросе, в частности после findByPk(). Экземпляр модели создается в методе CActiveRecord::instantiate(). Переопределим его в нашей модели.
<?php
class Expence extends CActiveRecord {
//...какой-то код
protected function instantiate($attributes) {
if(($record = ObjectWatcher::getRecord(get_class($this), $attributes['id'])) != false){ // Пытаемся получить объект, если он уже был запрошен ранее
$model = $record;
}else{// иначе просто создаем экземпляр класса на основе данных из базы
$model = parent::instantiate($attributes);
ObjectWatcher::addRecord($model, $attributes['id']);// добавляем модель в реестр
}
return $model;
}
//...какой-то код
}
Вот и все, теперь
$modelOne = Expence::model()->findByPk(10);
$modelTwo = Expence::model()->findByPk(10);
var_dump($modelOne === $modelTwo); // true
Ссылки: