Доброго времени суток, читатели!
Проблема
При работе с базой данных или просто с объектами, доступными из разных частей вашего приложения, есть опасность, что объекты, которые, казалось бы, должны быть равны вовсе таковыми не являются.
Например, допустим, у нас есть некоторая модель 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
Ссылки:
Автор: organium