ActiveRecord.php 8.87 KB
Newer Older
Carsten Brandt committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
Carsten Brandt committed
5 6 7
 * @license http://www.yiiframework.com/license/
 */

8
namespace yii\redis;
Carsten Brandt committed
9

10
use yii\base\InvalidConfigException;
11
use yii\base\NotSupportedException;
12
use yii\db\TableSchema;
13
use yii\helpers\StringHelper;
14

Carsten Brandt committed
15 16 17 18 19 20
/**
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
 *
 * @author Carsten Brandt <mail@cebe.cc>
 * @since 2.0
 */
21
class ActiveRecord extends \yii\db\ActiveRecord
Carsten Brandt committed
22
{
23 24 25
	/**
	 * @var array cache for TableSchema instances
	 */
26
	private static $_tables = [];
27

Carsten Brandt committed
28 29
	/**
	 * Returns the database connection used by this AR class.
30
	 * By default, the "redis" application component is used as the database connection.
Carsten Brandt committed
31 32 33 34 35
	 * You may override this method if you want to use a different database connection.
	 * @return Connection the database connection used by this AR class.
	 */
	public static function getDb()
	{
36
		return \Yii::$app->getComponent('redis');
Carsten Brandt committed
37 38 39
	}

	/**
Carsten Brandt committed
40
	 * @inheritdoc
Carsten Brandt committed
41
	 */
42
	public static function findBySql($sql, $params = [])
Carsten Brandt committed
43
	{
44
		throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord');
Carsten Brandt committed
45 46
	}

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
	/**
	 * @inheritDoc
	 */
	public static function createQuery()
	{
		return new ActiveQuery(['modelClass' => get_called_class()]);
	}

	/**
	 * @inheritDoc
	 */
	protected function createActiveRelation($config = [])
	{
		return new ActiveRelation($config);
	}

	/**
	 * Declares the name of the database table associated with this AR class.
	 * @return string the table name
	 */
	public static function tableName()
	{
		return static::getTableSchema()->name;
	}

	/**
	 * This method is ment to be overridden in redis ActiveRecord subclasses to return a [[RecordSchema]] instance.
	 * @return RecordSchema
	 * @throws \yii\base\InvalidConfigException
	 */
	public static function getRecordSchema()
	{
		throw new InvalidConfigException(__CLASS__.'::getRecordSchema() needs to be overridden in subclasses and return a RecordSchema.');
	}

	/**
	 * Returns the schema information of the DB table associated with this AR class.
	 * @return TableSchema the schema information of the DB table associated with this AR class.
	 */
	public static function getTableSchema()
	{
		$class = get_called_class();
		if (isset(self::$_tables[$class])) {
			return self::$_tables[$class];
		}
		return self::$_tables[$class] = static::getRecordSchema();
	}

	/**
	 * @inheritDocs
	 */
	public function insert($runValidation = true, $attributes = null)
	{
		if ($runValidation && !$this->validate($attributes)) {
			return false;
		}
		if ($this->beforeSave(true)) {
			$db = static::getDb();
			$values = $this->getDirtyAttributes($attributes);
			$pk = [];
//			if ($values === []) {
			foreach ($this->primaryKey() as $key) {
				$pk[$key] = $values[$key] = $this->getAttribute($key);
				if ($pk[$key] === null) {
					$pk[$key] = $values[$key] = $db->executeCommand('INCR', [static::tableName() . ':s:' . $key]);
					$this->setAttribute($key, $values[$key]);
				}
			}
//			}
			// save pk in a findall pool
			$db->executeCommand('RPUSH', [static::tableName(), static::buildKey($pk)]);

			$key = static::tableName() . ':a:' . static::buildKey($pk);
			// save attributes
			$args = [$key];
			foreach($values as $attribute => $value) {
				$args[] = $attribute;
				$args[] = $value;
			}
			$db->executeCommand('HMSET', $args);

			$this->setOldAttributes($values);
			$this->afterSave(true);
			return true;
		}
		return false;
	}

135 136 137 138 139
	/**
	 * Updates the whole table using the provided attribute values and conditions.
	 * For example, to change the status to be 1 for all customers whose status is 2:
	 *
	 * ~~~
140
	 * Customer::updateAll(['status' => 1], ['id' => 2]);
141 142 143
	 * ~~~
	 *
	 * @param array $attributes attribute values (name-value pairs) to be saved into the table
144 145 146
	 * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
	 * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
	 * @param array $params this parameter is ignored in redis implementation.
147 148
	 * @return integer the number of rows updated
	 */
149
	public static function updateAll($attributes, $condition = null, $params = [])
150 151 152 153
	{
		if (empty($attributes)) {
			return 0;
		}
Carsten Brandt committed
154
		$db = static::getDb();
155
		$n=0;
156 157 158 159
		foreach(static::fetchPks($condition) as $pk) {
			$newPk = $pk;
			$pk = static::buildKey($pk);
			$key = static::tableName() . ':a:' . $pk;
160
			// save attributes
161
			$args = [$key];
162
			foreach($attributes as $attribute => $value) {
163 164 165
				if (isset($newPk[$attribute])) {
					$newPk[$attribute] = $value;
				}
166 167 168
				$args[] = $attribute;
				$args[] = $value;
			}
169 170
			$newPk = static::buildKey($newPk);
			$newKey = static::tableName() . ':a:' . $newPk;
171
			// rename index if pk changed
172
			if ($newPk != $pk) {
173 174
				$db->executeCommand('MULTI');
				$db->executeCommand('HMSET', $args);
175 176 177
				$db->executeCommand('LINSERT', [static::tableName(), 'AFTER', $pk, $newPk]);
				$db->executeCommand('LREM', [static::tableName(), 0, $pk]);
				$db->executeCommand('RENAME', [$key, $newKey]);
178 179 180
				$db->executeCommand('EXEC');
			} else {
				$db->executeCommand('HMSET', $args);
181
			}
182 183 184 185 186 187 188 189 190 191
			$n++;
		}
		return $n;
	}

	/**
	 * Updates the whole table using the provided counter changes and conditions.
	 * For example, to increment all customers' age by 1,
	 *
	 * ~~~
192
	 * Customer::updateAllCounters(['age' => 1]);
193 194 195 196
	 * ~~~
	 *
	 * @param array $counters the counters to be updated (attribute name => increment value).
	 * Use negative values if you want to decrement the counters.
197 198 199
	 * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
	 * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
	 * @param array $params this parameter is ignored in redis implementation.
200 201
	 * @return integer the number of rows updated
	 */
202
	public static function updateAllCounters($counters, $condition = null, $params = [])
203
	{
Carsten Brandt committed
204 205 206
		if (empty($counters)) {
			return 0;
		}
207 208
		$db = static::getDb();
		$n=0;
209 210
		foreach(static::fetchPks($condition) as $pk) {
			$key = static::tableName() . ':a:' . static::buildKey($pk);
211
			foreach($counters as $attribute => $value) {
212
				$db->executeCommand('HINCRBY', [$key, $attribute, $value]);
213 214 215 216 217 218 219 220 221 222 223 224 225
			}
			$n++;
		}
		return $n;
	}

	/**
	 * Deletes rows in the table using the provided conditions.
	 * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
	 *
	 * For example, to delete all customers whose status is 3:
	 *
	 * ~~~
226
	 * Customer::deleteAll(['status' => 3]);
227 228
	 * ~~~
	 *
229 230 231
	 * @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL.
	 * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
	 * @param array $params this parameter is ignored in redis implementation.
232 233
	 * @return integer the number of rows deleted
	 */
234
	public static function deleteAll($condition = null, $params = [])
235 236
	{
		$db = static::getDb();
237
		$attributeKeys = [];
238 239 240
		$pks = static::fetchPks($condition);
		$db->executeCommand('MULTI');
		foreach($pks as $pk) {
241
			$pk = static::buildKey($pk);
242
			$db->executeCommand('LREM', [static::tableName(), 0, $pk]);
Carsten Brandt committed
243
			$attributeKeys[] = static::tableName() . ':a:' . $pk;
244
		}
245
		if (empty($attributeKeys)) {
246
			$db->executeCommand('EXEC');
247 248
			return 0;
		}
249 250 251
		$db->executeCommand('DEL', $attributeKeys);
		$result = $db->executeCommand('EXEC');
		return end($result);
252 253
	}

254 255 256 257 258 259 260
	private static function fetchPks($condition)
	{
		$query = static::createQuery();
		$query->where($condition);
		$records = $query->asArray()->all(); // TODO limit fetched columns to pk
		$primaryKey = static::primaryKey();

261
		$pks = [];
262
		foreach($records as $record) {
263
			$pk = [];
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
			foreach($primaryKey as $key) {
				$pk[$key] = $record[$key];
			}
			$pks[] = $pk;
		}
		return $pks;
	}

	/**
	 * Builds a normalized key from a given primary key value.
	 *
	 * @param mixed $key the key to be normalized
	 * @return string the generated key
	 */
	public static function buildKey($key)
	{
		if (is_numeric($key)) {
			return $key;
		} elseif (is_string($key)) {
			return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key);
		} elseif (is_array($key)) {
			if (count($key) == 1) {
				return self::buildKey(reset($key));
			}
288
			ksort($key); // ensure order is always the same
289 290 291 292 293 294 295 296 297 298 299 300 301
			$isNumeric = true;
			foreach($key as $value) {
				if (!is_numeric($value)) {
					$isNumeric = false;
				}
			}
			if ($isNumeric) {
				return implode('-', $key);
			}
		}
		return md5(json_encode($key));
	}

302
	/**
303 304 305 306
	 * Returns a value indicating whether the specified operation is transactional in the current [[scenario]].
	 * This method will always return false as transactional operations are not supported by redis.
	 * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]].
	 * @return boolean whether the specified operation is transactional in the current [[scenario]].
307
	 */
308
	public function isTransactional($operation)
309
	{
310
		return false;
311
	}
Carsten Brandt committed
312
}