Использование ActiveRedord от Yii в игре тайм менеджере

от автора

Всем привет!

Сегодня я хочу вам рассказать, как была реализована работа с кэшем в социальное игре тайм менеджере. Можете считать эту статью продолжением вот этой.

Напомню, что в проекте используется php(Yii), mysql и memcached. В проекте достаточно много сущностей, для каждой из которой есть своя модель, которая наследуется от CActiveRecord.

Хранятся файлы моделей следующим образом. В папке models создаем папку base. Когда генерируем модель через Gii, то указываем, что ее нужно положить в папку models/base и к имени класса добавляем Base. Затем создаем в models аналогичный класс без Base, который наследуется от базового класса и имеет в себе лишь метод model().

Кстати заранее скажу, что базовые модели наследуем не от CActiveRecord, а от ExtActiveRecord — расширяем CActiveRecord под наши нужды. Но об этом позже. Пока что разницы никакой.

Пример:
models/base/BaseUser.php — стандартный класс, который генерируется через Gii
models/User.php — класс, который наследуется от BaseUser и имеет в себе метод model()

	/** 	 * Returns the static model of the specified AR class. 	 * @param string $className active record class name. 	 * @return User the static model class 	 */ 	public static function model($className=__CLASS__) { 		return parent::model($className); 	} 	

Данная схема используется для того, чтобы в случае повторной генерации файла модели не потерять свой код и просто не забивать пространство стандартными кодом от Yii.

Не забываем добавить в конфиге ‘application.models.base.*’.

Перейдем собственно к теме поста и поставим задачи, которые хотим решить:

  1. Уменьшить количество запросов в базу на обновление
  2. Уменьшить количество запросов в базу на выборку

Уменьшаем количество запросов в базу на обновление

Как вы помните по прошлой статье, у нас используется очередь для выполнения команд. И для какого-то конкретного пользователя может требоваться выполнение более 2х команд последовательно. К примеру у нас приходит пачка из 3х команд: увеличить опыт, купить здание и поменять имя игрока. Предположим, что опыт, деньги и имя хранится в одной таблице user.

Реализация обработки очереди такова, что команды ничего не знают друг о друге и в стандартной реализации каждая команда будет загружать модель пользователя, изменять ее и сохранять.

Мы сейчас сделаем так, что подгрузка пользователя произойдет при первом к нему обращении, а сохранение только после выполнения всех команд. Для этого мы сделаем реестр моделей.
Это такая штука, к которой мы будем обращаться, чтобы получить модель User, вместо того, чтобы писать User::model()->findByPk().

Реестр моделей будет являться компонентом и будет прописан в конфиге в components

'components' => array( 	// ... 	'modelRegistry'=>array( 		'class' => 'ModelRegistry' 	) 	// ... )	 

Сам класс выглядит следующим образом

class ModelRegistry { 	protected $registries = array();  	public function init() {}  	/** 	 * Возвращает реестр выбранной модели 	 * @param string $name 	 * @param mixed $attr 	 * @return ExtActiveRecord 	 */ 	public function & registry($name, $attr = array()) {		 		$key = $name . md5(serialize($attr)); 		if (!isset($this->registries[$key])) { 			$model = ucfirst($name); 			$obj = $model::model();  			if (!is_array($attr)) $attr = array($attr);  			$this->registries[$key] = call_user_func_array(array(&$obj, 'registry'), $attr); 		}  		// будет возвращена ссылка на объект в массиве благодаря & в имени функции 		return $this->registries[$key]; 	}  	/** 	 * Сохранение изменений 	 */ 	public function saveAll() { 		foreach ($this->registries as $obj) { 			$obj->save(); 		} 	} } 

У каждой нашей модели, которую мы хотим получить через реестр моделей будет метод registry, который будет возвращать объект. User в данном случае выглядит следующим образом

/**  * @property integer $id  * @property integer $exp  * @property integer $money  * @property integer $name  */ class User extends BaseUser { 	/** 	 * Returns the static model of the specified AR class. 	 * @param string $className active record class name. 	 * @return User the static model class 	 */ 	public static function model($className=__CLASS__) { 		return parent::model($className); 	}  	/** 	 * Получает профиль игрока 	 * @param int $userID 	 * @return User|bool 	 */ 	public function registry($userID) { 		if ($obj = $this->findByPk($userID)) { 			$res = $obj; 		} else { 			$res = false; 		}  		return $res; 	} } 

К чему это все привело. К примеру у нас есть контроллер, который вызывает два метода, изменяющие пользователя.

	/** 	 * @var ModelRegistry 	 */ 	protected $reg;  	public function actionRun() { 		$userID = 1;  		$this->reg = &Yii::app()->modelRegistry; 		$this->firstChange($userID); 		$this->secondChange($userID); 		$this->reg->saveAll(); 	}  	public function firstChange($userID) { 		// здесь первой обращение. Реестр создаст модель и подгрузит данные из базы 		// & нужно, чтобы получить ссылку на объект из массива 		$user = &$this->reg->registry('user', $userID); 		$user->exp = 10; 	}  	public function secondChange($userID) { 		// здесь обращение к тому, что уже подгружено в реест. В базу обращения нет 		// & аналогично 		$user = &$this->reg->registry('user', $userID); 		$user->money = 20; 	} 

Собственно будет одно обращение в базу на select и одно на update.
Здесь мы сталкиваемся с дополнительной задачей. Если вызвать этот код второй раз, то поля пользователя остануться теми же, но сохранение все равно будет произведено. Нам нужно сделать так, чтобы наш ActiveRecord сохранялся только в том случае, когда объект притерпел изменения.

Тут нам на помощь приходит наш ExtActiveRecord, который мы использовали для расширения CActiveRecord.

class ExtActiveRecord extends CActiveRecord {  	protected $_oldAttributes = array();  	/** 	 * Тот самый метод, который должен быть во всех моделях 	 */ 	public function registry() {}  	/** 	 * Запомнить текущее состояние модели 	 */ 	public function memoryAttributes() { 		$this->_oldAttributes = $this->attributes; 	}  	/** 	 * Поиск изменений в моделе. Возвращает список измененных полей 	 * Если объект был только создан и его нужно будет сохранить полностью, то возвращает false 	 * @return array|false 	 */ 	protected function getChanges() { 		$res = array(); 		if (empty($this->_oldAttributes)) { 			$res = false; 		} else { 			foreach ($this->_oldAttributes as $key => $value) { 				if ($this->$key != $value) { 					$res[] = $key; 				} 			} 		}  		return $res; 	}  	/** 	 * Сохраняем только изменения 	 * @return bool 	 */ 	public function save() { 		if (($attr = $this->getChanges()) === false) { 			$res = parent::save(); 		} elseif ($attr) { 			$res = $this->update($attr); 		} else { 			$res = false; 		}  		return $res; 	} } 

И обновляем метод registry в модели User

public function registry($userID) { 		if ($obj = $this->findByPk($userID)) { 			$res = $obj; 		} else { 			$res = false; 		}  		if ($res) { 			// запоминаем текущее состояние модели 			$res->memoryAttributes(); 		}  		return $res; 	} 

Собственно теперь будет производиться только insert или update измененных полей модели.
Оставляю вам пространство для творчества и позволяю самим разобраться в том, как создать нового пользователя и поместить его в реест моделей во время выполнения скрипта.

Я показал вам, как можно хранить в реестре какие-либо объекты. Но иногда возникают ситуации, когда нам нужно хранить там какой-то список. К примеру для каждого пользователя у нас есть 10 машин. И мы хотим, чтобы в реестре было не 10 машин, а один объект, содержащий все машины. Для этого используется класс ModelList, который хранит модели машин.

class ModelList {  	/** 	 * @var array с данными ExtActiveRecord 	 */ 	public $list = array();  	/** 	 * Создает новый список 	 * @param array|bool $list массив с ExtActiveRecord 	 * @return ModelList 	 */ 	public static function make($list = array()) { 		if (!is_array($list) && empty($list)) { 			$list = array(); 		} 		$obj = new ModelList(); 		$obj->list = $list;  		return $obj; 	}  	/** 	 * Добавить объект в список 	 * @param ExtActiveRecord $obj 	 */ 	public function pushObject($obj) { 		$this->list[] = $obj; 	}  	/** 	 * Вызвать у всех моделей метод 	 * @param string $name 	 */ 	public function callMethod($name) { 		foreach ($this->list as &$obj) { 			$obj->$name(); 		} 	}  	/** 	 * Сохранение всех объектов 	 */ 	public function save() { 		$this->callMethod('save'); 	} } 

А вот так выглядит модель машины

<?php  /**  * @property integer $id  * @property integer $user_id  * @property integer $car_id  * @property integer $speed  */ class Car extends BaseCar { 	/** 	 * Returns the static model of the specified AR class. 	 * @param string $className active record class name. 	 * @return Car the static model class 	 */ 	public static function model($className=__CLASS__) { 		return parent::model($className); 	}  	/** 	 * Получает реестр всех машин пользователя 	 * @param int $userID 	 * @return ModelList 	 */ 	public function registry($userID) { 		$list = $this->findAllByAttributes(array('user_id'=>$userID)); 		$res = ModelList::make($list);  		// у всех машин сохраняем состояние 		$res->callMethod('memoryAttributes');  		return $res; 	}  	/** 	 * Создаем, но не сохраняем. Используется, когда мы хотим положить в ModelList через pushObject 	 * @param int $userID 	 * @param int $carID 	 * @return Car 	 */ 	public static function make($userID, $carID) { 		$obj = new Car(); 		$obj->user_id = $userID; 		$obj->car_id = $dict->area_id; 		$obj->speed = 10; 		return $obj; 	} } 

Собственно теперь, когда мы выполним следующий код,

$carList = &Yii::app()->modelRegistry->registry('car', 1); 

то получим объект класса ModelList, который будет содержать в себе все машины игрока. Их так же можно изменять (не забывая обращаться по ссылке в $carList->list) и потом сохранять через реест моделей стандартным saveAll.

Так как статья выходит довольно большая, то про кэширование всего этого я расскажу в следующей статье.
Могу сказать, что в идеальных условиях с кэшированием данная реализация не обращается за одними и теми же данными два раза, даже если после первого раза было произведено их обновление.

Данный вариант работы с моделями удобно используется в нашем проекте, но возможно не подойдет вам.
Все что я хотел, так это просто показать какие бывают задачи и как их можно решать.

ссылка на оригинал статьи http://habrahabr.ru/company/alawar/blog/177181/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *