Commit a95d54cc by Qiang Xue

Merge pull request #905 from yiisoft/redis

[WIP] Redis ActiveRecord
parents 863f5238 ed98df5c
...@@ -229,14 +229,13 @@ class Model extends Component implements IteratorAggregate, ArrayAccess ...@@ -229,14 +229,13 @@ class Model extends Component implements IteratorAggregate, ArrayAccess
* You may override this method to change the default behavior. * You may override this method to change the default behavior.
* @return array list of attribute names. * @return array list of attribute names.
*/ */
public function attributes() public static function attributes()
{ {
$class = new ReflectionClass($this); $class = new ReflectionClass(get_called_class());
$names = []; $names = [];
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$name = $property->getName();
if (!$property->isStatic()) { if (!$property->isStatic()) {
$names[] = $name; $names[] = $property->getName();
} }
} }
return $names; return $names;
......
...@@ -568,9 +568,9 @@ class ActiveRecord extends Model ...@@ -568,9 +568,9 @@ class ActiveRecord extends Model
* The default implementation will return all column names of the table associated with this AR class. * The default implementation will return all column names of the table associated with this AR class.
* @return array list of attribute names. * @return array list of attribute names.
*/ */
public function attributes() public static function attributes()
{ {
return array_keys($this->getTableSchema()->columns); return array_keys(static::getTableSchema()->columns);
} }
/** /**
...@@ -580,7 +580,7 @@ class ActiveRecord extends Model ...@@ -580,7 +580,7 @@ class ActiveRecord extends Model
*/ */
public function hasAttribute($name) public function hasAttribute($name)
{ {
return isset($this->_attributes[$name]) || isset($this->getTableSchema()->columns[$name]); return isset($this->_attributes[$name]) || in_array($name, $this->attributes());
} }
/** /**
...@@ -1244,7 +1244,7 @@ class ActiveRecord extends Model ...@@ -1244,7 +1244,7 @@ class ActiveRecord extends Model
public static function create($row) public static function create($row)
{ {
$record = static::instantiate($row); $record = static::instantiate($row);
$columns = static::getTableSchema()->columns; $columns = array_flip(static::attributes());
foreach ($row as $name => $value) { foreach ($row as $name => $value) {
if (isset($columns[$name])) { if (isset($columns[$name])) {
$record->_attributes[$name] = $value; $record->_attributes[$name] = $value;
...@@ -1299,7 +1299,7 @@ class ActiveRecord extends Model ...@@ -1299,7 +1299,7 @@ class ActiveRecord extends Model
if ($relation instanceof ActiveRelationInterface) { if ($relation instanceof ActiveRelationInterface) {
return $relation; return $relation;
} else { } else {
return null; throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".');
} }
} catch (UnknownMethodException $e) { } catch (UnknownMethodException $e) {
throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
...@@ -1336,9 +1336,7 @@ class ActiveRecord extends Model ...@@ -1336,9 +1336,7 @@ class ActiveRecord extends Model
if (is_array($relation->via)) { if (is_array($relation->via)) {
/** @var ActiveRelation $viaRelation */ /** @var ActiveRelation $viaRelation */
list($viaName, $viaRelation) = $relation->via; list($viaName, $viaRelation) = $relation->via;
/** @var ActiveRecord $viaClass */
$viaClass = $viaRelation->modelClass; $viaClass = $viaRelation->modelClass;
$viaTable = $viaClass::tableName();
// unset $viaName so that it can be reloaded to reflect the change // unset $viaName so that it can be reloaded to reflect the change
unset($this->_related[$viaName]); unset($this->_related[$viaName]);
} else { } else {
...@@ -1355,8 +1353,19 @@ class ActiveRecord extends Model ...@@ -1355,8 +1353,19 @@ class ActiveRecord extends Model
foreach ($extraColumns as $k => $v) { foreach ($extraColumns as $k => $v) {
$columns[$k] = $v; $columns[$k] = $v;
} }
static::getDb()->createCommand() if (is_array($relation->via)) {
->insert($viaTable, $columns)->execute(); /** @var $viaClass ActiveRecord */
/** @var $record ActiveRecord */
$record = new $viaClass();
foreach($columns as $column => $value) {
$record->$column = $value;
}
$record->insert(false);
} else {
/** @var $viaTable string */
static::getDb()->createCommand()
->insert($viaTable, $columns)->execute();
}
} else { } else {
$p1 = $model->isPrimaryKey(array_keys($relation->link)); $p1 = $model->isPrimaryKey(array_keys($relation->link));
$p2 = $this->isPrimaryKey(array_values($relation->link)); $p2 = $this->isPrimaryKey(array_values($relation->link));
...@@ -1411,9 +1420,7 @@ class ActiveRecord extends Model ...@@ -1411,9 +1420,7 @@ class ActiveRecord extends Model
if (is_array($relation->via)) { if (is_array($relation->via)) {
/** @var ActiveRelation $viaRelation */ /** @var ActiveRelation $viaRelation */
list($viaName, $viaRelation) = $relation->via; list($viaName, $viaRelation) = $relation->via;
/** @var ActiveRecord $viaClass */
$viaClass = $viaRelation->modelClass; $viaClass = $viaRelation->modelClass;
$viaTable = $viaClass::tableName();
unset($this->_related[$viaName]); unset($this->_related[$viaName]);
} else { } else {
$viaRelation = $relation->via; $viaRelation = $relation->via;
...@@ -1426,15 +1433,29 @@ class ActiveRecord extends Model ...@@ -1426,15 +1433,29 @@ class ActiveRecord extends Model
foreach ($relation->link as $a => $b) { foreach ($relation->link as $a => $b) {
$columns[$b] = $model->$a; $columns[$b] = $model->$a;
} }
$command = static::getDb()->createCommand(); if (is_array($relation->via)) {
if ($delete) { /** @var $viaClass ActiveRecord */
$command->delete($viaTable, $columns)->execute(); if ($delete) {
$viaClass::deleteAll($columns);
} else {
$nulls = [];
foreach (array_keys($columns) as $a) {
$nulls[$a] = null;
}
$viaClass::updateAll($nulls, $columns);
}
} else { } else {
$nulls = []; /** @var $viaTable string */
foreach (array_keys($columns) as $a) { $command = static::getDb()->createCommand();
$nulls[$a] = null; if ($delete) {
$command->delete($viaTable, $columns)->execute();
} else {
$nulls = [];
foreach (array_keys($columns) as $a) {
$nulls[$a] = null;
}
$command->update($viaTable, $nulls, $columns)->execute();
} }
$command->update($viaTable, $nulls, $columns)->execute();
} }
} else { } else {
$p1 = $model->isPrimaryKey(array_keys($relation->link)); $p1 = $model->isPrimaryKey(array_keys($relation->link));
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\redis;
use yii\base\InvalidConfigException;
use yii\base\NotSupportedException;
use yii\helpers\StringHelper;
/**
* ActiveRecord is the base class for classes representing relational data in terms of objects.
*
* This class implements the ActiveRecord pattern for the [redis](http://redis.io/) key-value store.
*
* For defining a record a subclass should at least implement the [[attributes()]] method to define
* attributes. A primary key can be defined via [[primaryKey()]] which defaults to `id` if not specified.
*
* The following is an example model called `Customer`:
*
* ```php
* class Customer extends \yii\redis\ActiveRecord
* {
* public function attributes()
* {
* return ['id', 'name', 'address', 'registration_date'];
* }
* }
* ```
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ActiveRecord extends \yii\db\ActiveRecord
{
/**
* Returns the database connection used by this AR class.
* By default, the "redis" application component is used as the database connection.
* 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()
{
return \Yii::$app->getComponent('redis');
}
/**
* @inheritDoc
*/
public static function createQuery()
{
return new ActiveQuery(['modelClass' => get_called_class()]);
}
/**
* @inheritDoc
*/
protected function createActiveRelation($config = [])
{
return new ActiveRelation($config);
}
/**
* Returns the primary key name(s) for this AR class.
* This method should be overridden by child classes to define the primary key.
*
* Note that an array should be returned even when it is a single primary key.
*
* @return string[] the primary keys of this record.
*/
public static function primaryKey()
{
return ['id'];
}
/**
* Returns the list of all attribute names of the model.
* This method must be overridden by child classes to define available attributes.
* @return array list of attribute names.
*/
public static function attributes()
{
throw new InvalidConfigException('The attributes() method of redis ActiveRecord has to be implemented by child classes.');
}
/**
* @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;
}
/**
* 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:
*
* ~~~
* Customer::updateAll(['status' => 1], ['id' => 2]);
* ~~~
*
* @param array $attributes attribute values (name-value pairs) to be saved into the table
* @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.
* @return integer the number of rows updated
*/
public static function updateAll($attributes, $condition = null, $params = [])
{
if (empty($attributes)) {
return 0;
}
$db = static::getDb();
$n=0;
foreach(static::fetchPks($condition) as $pk) {
$newPk = $pk;
$pk = static::buildKey($pk);
$key = static::tableName() . ':a:' . $pk;
// save attributes
$args = [$key];
foreach($attributes as $attribute => $value) {
if (isset($newPk[$attribute])) {
$newPk[$attribute] = $value;
}
$args[] = $attribute;
$args[] = $value;
}
$newPk = static::buildKey($newPk);
$newKey = static::tableName() . ':a:' . $newPk;
// rename index if pk changed
if ($newPk != $pk) {
$db->executeCommand('MULTI');
$db->executeCommand('HMSET', $args);
$db->executeCommand('LINSERT', [static::tableName(), 'AFTER', $pk, $newPk]);
$db->executeCommand('LREM', [static::tableName(), 0, $pk]);
$db->executeCommand('RENAME', [$key, $newKey]);
$db->executeCommand('EXEC');
} else {
$db->executeCommand('HMSET', $args);
}
$n++;
}
return $n;
}
/**
* Updates the whole table using the provided counter changes and conditions.
* For example, to increment all customers' age by 1,
*
* ~~~
* Customer::updateAllCounters(['age' => 1]);
* ~~~
*
* @param array $counters the counters to be updated (attribute name => increment value).
* Use negative values if you want to decrement the counters.
* @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.
* @return integer the number of rows updated
*/
public static function updateAllCounters($counters, $condition = null, $params = [])
{
if (empty($counters)) {
return 0;
}
$db = static::getDb();
$n=0;
foreach(static::fetchPks($condition) as $pk) {
$key = static::tableName() . ':a:' . static::buildKey($pk);
foreach($counters as $attribute => $value) {
$db->executeCommand('HINCRBY', [$key, $attribute, $value]);
}
$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:
*
* ~~~
* Customer::deleteAll(['status' => 3]);
* ~~~
*
* @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.
* @return integer the number of rows deleted
*/
public static function deleteAll($condition = null, $params = [])
{
$db = static::getDb();
$attributeKeys = [];
$pks = static::fetchPks($condition);
$db->executeCommand('MULTI');
foreach($pks as $pk) {
$pk = static::buildKey($pk);
$db->executeCommand('LREM', [static::tableName(), 0, $pk]);
$attributeKeys[] = static::tableName() . ':a:' . $pk;
}
if (empty($attributeKeys)) {
$db->executeCommand('EXEC');
return 0;
}
$db->executeCommand('DEL', $attributeKeys);
$result = $db->executeCommand('EXEC');
return end($result);
}
private static function fetchPks($condition)
{
$query = static::createQuery();
$query->where($condition);
$records = $query->asArray()->all(); // TODO limit fetched columns to pk
$primaryKey = static::primaryKey();
$pks = [];
foreach($records as $record) {
$pk = [];
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));
}
ksort($key); // ensure order is always the same
$isNumeric = true;
foreach($key as $value) {
if (!is_numeric($value)) {
$isNumeric = false;
}
}
if ($isNumeric) {
return implode('-', $key);
}
}
return md5(json_encode($key));
}
/**
* @inheritdoc
*/
public static function getTableSchema()
{
throw new NotSupportedException('getTableSchema() is not supported by redis ActiveRecord');
}
/**
* @inheritdoc
*/
public static function findBySql($sql, $params = [])
{
throw new NotSupportedException('findBySql() is not supported by redis ActiveRecord');
}
/**
* 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]].
*/
public function isTransactional($operation)
{
return false;
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\redis;
use yii\db\ActiveRelationInterface;
use yii\db\ActiveRelationTrait;
/**
* ActiveRelation represents a relation between two Active Record classes.
*
* ActiveRelation instances are usually created by calling [[ActiveRecord::hasOne()]] and
* [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
* a getter method which calls one of the above methods and returns the created ActiveRelation object.
*
* A relation is specified by [[link]] which represents the association between columns
* of different tables; and the multiplicity of the relation is indicated by [[multiple]].
*
* If a relation involves a pivot table, it may be specified by [[via()]] or [[viaTable()]] method.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ActiveRelation extends ActiveQuery implements ActiveRelationInterface
{
use ActiveRelationTrait;
/**
* Executes a script created by [[LuaScriptBuilder]]
* @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `db` application component will be used.
* @param string $type the type of the script to generate
* @param null $column
* @return array|bool|null|string
*/
protected function executeScript($db, $type, $column=null)
{
if ($this->primaryModel !== null) {
// lazy loading
if ($this->via instanceof self) {
// via pivot table
$viaModels = $this->via->findPivotRows([$this->primaryModel]);
$this->filterByModels($viaModels);
} elseif (is_array($this->via)) {
// via relation
/** @var ActiveRelation $viaQuery */
list($viaName, $viaQuery) = $this->via;
if ($viaQuery->multiple) {
$viaModels = $viaQuery->all();
$this->primaryModel->populateRelation($viaName, $viaModels);
} else {
$model = $viaQuery->one();
$this->primaryModel->populateRelation($viaName, $model);
$viaModels = $model === null ? [] : [$model];
}
$this->filterByModels($viaModels);
} else {
$this->filterByModels([$this->primaryModel]);
}
}
return parent::executeScript($db, $type, $column);
}
}
<?php <?php
/** /**
* Connection class file
*
* @link http://www.yiiframework.com/ * @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008 Yii Software LLC * @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/ * @license http://www.yiiframework.com/license/
*/ */
...@@ -22,8 +20,7 @@ use yii\helpers\Inflector; ...@@ -22,8 +20,7 @@ use yii\helpers\Inflector;
* *
* @property string $driverName Name of the DB driver. This property is read-only. * @property string $driverName Name of the DB driver. This property is read-only.
* @property boolean $isActive Whether the DB connection is established. This property is read-only. * @property boolean $isActive Whether the DB connection is established. This property is read-only.
* @property Transaction $transaction The currently active transaction. Null if no active transaction. This * @property LuaScriptBuilder $luaScriptBuilder This property is read-only.
* property is read-only.
* *
* @author Carsten Brandt <mail@cebe.cc> * @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
...@@ -203,10 +200,6 @@ class Connection extends Component ...@@ -203,10 +200,6 @@ class Connection extends Component
'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key 'ZUNIONSTORE', // destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] Add multiple sorted sets and store the resulting sorted set in a new key
]; ];
/** /**
* @var Transaction the currently active transaction
*/
private $_transaction;
/**
* @var resource redis socket connection * @var resource redis socket connection
*/ */
private $_socket; private $_socket;
...@@ -283,7 +276,6 @@ class Connection extends Component ...@@ -283,7 +276,6 @@ class Connection extends Component
$this->executeCommand('QUIT'); $this->executeCommand('QUIT');
stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR);
$this->_socket = null; $this->_socket = null;
$this->_transaction = null;
} }
} }
...@@ -298,27 +290,6 @@ class Connection extends Component ...@@ -298,27 +290,6 @@ class Connection extends Component
} }
/** /**
* Returns the currently active transaction.
* @return Transaction the currently active transaction. Null if no active transaction.
*/
public function getTransaction()
{
return $this->_transaction && $this->_transaction->isActive ? $this->_transaction : null;
}
/**
* Starts a transaction.
* @return Transaction the transaction initiated
*/
public function beginTransaction()
{
$this->open();
$this->_transaction = new Transaction(['db' => $this]);
$this->_transaction->begin();
return $this->_transaction;
}
/**
* Returns the name of the DB driver for the current [[dsn]]. * Returns the name of the DB driver for the current [[dsn]].
* @return string name of the DB driver * @return string name of the DB driver
*/ */
...@@ -332,6 +303,14 @@ class Connection extends Component ...@@ -332,6 +303,14 @@ class Connection extends Component
} }
/** /**
* @return LuaScriptBuilder
*/
public function getLuaScriptBuilder()
{
return new LuaScriptBuilder();
}
/**
* *
* @param string $name * @param string $name
* @param array $params * @param array $params
......
<?php
/**
* Transaction class file.
*
* @link http://www.yiiframework.com/
* @copyright Copyright &copy; 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\redis;
use yii\base\InvalidConfigException;
use yii\db\Exception;
/**
* Transaction represents a DB transaction.
*
* @property boolean $isActive Whether this transaction is active. Only an active transaction can [[commit()]]
* or [[rollBack()]]. This property is read-only.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class Transaction extends \yii\base\Object
{
/**
* @var Connection the database connection that this transaction is associated with.
*/
public $db;
/**
* @var boolean whether this transaction is active. Only an active transaction
* can [[commit()]] or [[rollBack()]]. This property is set true when the transaction is started.
*/
private $_active = false;
/**
* Returns a value indicating whether this transaction is active.
* @return boolean whether this transaction is active. Only an active transaction
* can [[commit()]] or [[rollBack()]].
*/
public function getIsActive()
{
return $this->_active;
}
/**
* Begins a transaction.
* @throws InvalidConfigException if [[connection]] is null
*/
public function begin()
{
if (!$this->_active) {
if ($this->db === null) {
throw new InvalidConfigException('Transaction::db must be set.');
}
\Yii::trace('Starting transaction', __CLASS__);
$this->db->open();
$this->db->createCommand('MULTI')->execute();
$this->_active = true;
}
}
/**
* Commits a transaction.
* @throws Exception if the transaction or the DB connection is not active.
*/
public function commit()
{
if ($this->_active && $this->db && $this->db->isActive) {
\Yii::trace('Committing transaction', __CLASS__);
$this->db->createCommand('EXEC')->execute();
// TODO handle result of EXEC
$this->_active = false;
} else {
throw new Exception('Failed to commit transaction: transaction was inactive.');
}
}
/**
* Rolls back a transaction.
* @throws Exception if the transaction or the DB connection is not active.
*/
public function rollback()
{
if ($this->_active && $this->db && $this->db->isActive) {
\Yii::trace('Rolling back transaction', __CLASS__);
$this->db->pdo->commit();
$this->_active = false;
} else {
throw new Exception('Failed to roll back transaction: transaction was inactive.');
}
}
}
...@@ -64,13 +64,13 @@ class UniqueValidator extends Validator ...@@ -64,13 +64,13 @@ class UniqueValidator extends Validator
$className = $this->className === null ? get_class($object) : $this->className; $className = $this->className === null ? get_class($object) : $this->className;
$attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $attributeName = $this->attributeName === null ? $attribute : $this->attributeName;
$table = $className::getTableSchema(); $attributes = $className::attributes();
if (($column = $table->getColumn($attributeName)) === null) { if (!in_array($attributeName, $attributes)) {
throw new InvalidConfigException("Table '{$table->name}' does not have a column named '$attributeName'."); throw new InvalidConfigException("'$className' does not have an attribute named '$attributeName'.");
} }
$query = $className::find(); $query = $className::find();
$query->where([$column->name => $value]); $query->where([$attributeName => $value]);
if (!$object instanceof ActiveRecord || $object->getIsNewRecord()) { if (!$object instanceof ActiveRecord || $object->getIsNewRecord()) {
// if current $object isn't in the database yet then it's OK just to call exists() // if current $object isn't in the database yet then it's OK just to call exists()
...@@ -82,7 +82,7 @@ class UniqueValidator extends Validator ...@@ -82,7 +82,7 @@ class UniqueValidator extends Validator
$n = count($objects); $n = count($objects);
if ($n === 1) { if ($n === 1) {
if ($column->isPrimaryKey) { if (in_array($attributeName, $className::primaryKey())) {
// primary key is modified and not unique // primary key is modified and not unique
$exists = $object->getOldPrimaryKey() != $object->getPrimaryKey(); $exists = $object->getOldPrimaryKey() != $object->getPrimaryKey();
} else { } else {
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\data\ar\redis;
use yii\redis\Connection;
/**
* ActiveRecord is ...
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class ActiveRecord extends \yii\redis\ActiveRecord
{
public static $db;
public static function getDb()
{
return self::$db;
}
}
\ No newline at end of file
<?php
namespace yiiunit\data\ar\redis;
class Customer extends ActiveRecord
{
const STATUS_ACTIVE = 1;
const STATUS_INACTIVE = 2;
public $status2;
public static function attributes()
{
return ['id', 'email', 'name', 'address', 'status'];
}
/**
* @return \yii\redis\ActiveRelation
*/
public function getOrders()
{
return $this->hasMany(Order::className(), ['customer_id' => 'id']);
}
public static function active($query)
{
$query->andWhere(['status' => 1]);
}
}
\ No newline at end of file
<?php
namespace yiiunit\data\ar\redis;
class Item extends ActiveRecord
{
public static function attributes()
{
return ['id', 'name', 'category_id'];
}
}
\ No newline at end of file
<?php
namespace yiiunit\data\ar\redis;
class Order extends ActiveRecord
{
public static function attributes()
{
return ['id', 'customer_id', 'create_time', 'total'];
}
public function getCustomer()
{
return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
}
public function getOrderItems()
{
return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
}
public function getItems()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems', function($q) {
// additional query configuration
});
}
public function getBooks()
{
return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems', ['order_id' => 'id']);
//->where(['category_id' => 1]);
}
public function beforeSave($insert)
{
if (parent::beforeSave($insert)) {
$this->create_time = time();
return true;
} else {
return false;
}
}
}
\ No newline at end of file
<?php
namespace yiiunit\data\ar\redis;
use yii\redis\RecordSchema;
class OrderItem extends ActiveRecord
{
public static function primaryKey()
{
return ['order_id', 'item_id'];
}
public static function attributes()
{
return ['order_id', 'item_id', 'quantity', 'subtotal'];
}
public function getOrder()
{
return $this->hasOne(Order::className(), ['id' => 'order_id']);
}
public function getItem()
{
return $this->hasOne(Item::className(), ['id' => 'item_id']);
}
}
\ No newline at end of file
...@@ -29,5 +29,9 @@ return [ ...@@ -29,5 +29,9 @@ return [
'password' => 'postgres', 'password' => 'postgres',
'fixture' => __DIR__ . '/postgres.sql', 'fixture' => __DIR__ . '/postgres.sql',
], ],
'redis' => [
'dsn' => 'redis://localhost:6379/0',
'password' => null,
],
], ],
]; ];
...@@ -40,6 +40,12 @@ class ActiveRecordTest extends DatabaseTestCase ...@@ -40,6 +40,12 @@ class ActiveRecordTest extends DatabaseTestCase
$customer = Customer::find(2); $customer = Customer::find(2);
$this->assertTrue($customer instanceof Customer); $this->assertTrue($customer instanceof Customer);
$this->assertEquals('user2', $customer->name); $this->assertEquals('user2', $customer->name);
$customer = Customer::find(5);
$this->assertNull($customer);
// query scalar
$customerName = Customer::find()->where(array('id' => 2))->select('name')->scalar();
$this->assertEquals('user2', $customerName);
// find by column values // find by column values
$customer = Customer::find(['id' => 2, 'name' => 'user2']); $customer = Customer::find(['id' => 2, 'name' => 'user2']);
......
<?php
namespace yiiunit\framework\redis;
use yii\redis\Connection;
/**
* @group redis
*/
class RedisConnectionTest extends RedisTestCase
{
/**
* Empty DSN should throw exception
* @expectedException \yii\base\InvalidConfigException
*/
public function testEmptyDSN()
{
$db = new Connection();
$db->open();
}
/**
* test connection to redis and selection of db
*/
public function testConnect()
{
$db = new Connection();
$db->dsn = 'redis://localhost:6379';
$db->open();
$this->assertTrue($db->ping());
$db->set('YIITESTKEY', 'YIITESTVALUE');
$db->close();
$db = new Connection();
$db->dsn = 'redis://localhost:6379/0';
$db->open();
$this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY'));
$db->close();
$db = new Connection();
$db->dsn = 'redis://localhost:6379/1';
$db->open();
$this->assertNull($db->get('YIITESTKEY'));
$db->close();
}
public function keyValueData()
{
return array(
array(123),
array(-123),
array(0),
array('test'),
array("test\r\ntest"),
array(''),
);
}
/**
* @dataProvider keyValueData
*/
public function testStoreGet($data)
{
$db = $this->getConnection(true);
$db->set('hi', $data);
$this->assertEquals($data, $db->get('hi'));
}
}
\ No newline at end of file
<?php
namespace yiiunit\framework\redis;
use yii\redis\Connection;
use yiiunit\TestCase;
/**
* RedisTestCase is the base class for all redis related test cases
*/
abstract class RedisTestCase extends TestCase
{
protected function setUp()
{
$this->mockApplication();
$databases = $this->getParam('databases');
$params = isset($databases['redis']) ? $databases['redis'] : null;
if ($params === null || !isset($params['dsn'])) {
$this->markTestSkipped('No redis server connection configured.');
}
$dsn = explode('/', $params['dsn']);
$host = $dsn[2];
if (strpos($host, ':')===false) {
$host .= ':6379';
}
if(!@stream_socket_client($host, $errorNumber, $errorDescription, 0.5)) {
$this->markTestSkipped('No redis server running at ' . $params['dsn'] . ' : ' . $errorNumber . ' - ' . $errorDescription);
}
parent::setUp();
}
/**
* @param bool $reset whether to clean up the test database
* @return Connection
*/
public function getConnection($reset = true)
{
$databases = $this->getParam('databases');
$params = isset($databases['redis']) ? $databases['redis'] : array();
$db = new Connection;
$db->dsn = $params['dsn'];
$db->password = $params['password'];
if ($reset) {
$db->open();
$db->flushall();
}
return $db;
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment