From 79982c98480bb52d18a7534e159e807978cc5b1b Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 24 Apr 2013 18:33:11 +0200 Subject: [PATCH] Redis Insert, Update, Delete and Find is ready and roughly unit tested relations are not working yet --- framework/db/redis/ActiveQuery.php | 34 +- framework/db/redis/ActiveRecord.php | 294 +++++++++++++-- framework/db/redis/ActiveRelation.php | 230 +++++++++++- framework/db/redis/Connection.php | 2 +- tests/unit/data/ar/redis/ActiveRecord.php | 26 ++ tests/unit/data/ar/redis/Customer.php | 35 ++ tests/unit/data/ar/redis/Item.php | 25 ++ tests/unit/data/ar/redis/Order.php | 63 ++++ tests/unit/data/ar/redis/OrderItem.php | 36 ++ tests/unit/framework/db/redis/ActiveRecordTest.php | 402 +++++++++++++++++++++ 10 files changed, 1111 insertions(+), 36 deletions(-) create mode 100644 tests/unit/data/ar/redis/ActiveRecord.php create mode 100644 tests/unit/data/ar/redis/Customer.php create mode 100644 tests/unit/data/ar/redis/Item.php create mode 100644 tests/unit/data/ar/redis/Order.php create mode 100644 tests/unit/data/ar/redis/OrderItem.php create mode 100644 tests/unit/framework/db/redis/ActiveRecordTest.php diff --git a/framework/db/redis/ActiveQuery.php b/framework/db/redis/ActiveQuery.php index b387421..1fbde46 100644 --- a/framework/db/redis/ActiveQuery.php +++ b/framework/db/redis/ActiveQuery.php @@ -80,8 +80,19 @@ class ActiveQuery extends \yii\base\Component */ public $primaryKeys; + /** + * List of multiple pks must be zero based + * + * @param $primaryKeys + * @return ActiveQuery + */ public function primaryKeys($primaryKeys) { - $this->primaryKeys = $primaryKeys; + if (is_array($primaryKeys) && isset($primaryKeys[0])) { + $this->primaryKeys = $primaryKeys; + } else { + $this->primaryKeys = array($primaryKeys); + } + return $this; } @@ -103,7 +114,12 @@ class ActiveQuery extends \yii\base\Component foreach($primaryKeys as $pk) { $key = $modelClass::tableName() . ':a:' . (is_array($pk) ? implode('-', $pk) : $pk); // TODO escape PK glue // get attributes - $rows[] = $db->executeCommand('HGETALL', array($key)); + $data = $db->executeCommand('HGETALL', array($key)); + $row = array(); + for($i=0;$icreateModels($rows); @@ -134,9 +150,15 @@ class ActiveQuery extends \yii\base\Component $pk = reset($primaryKeys); $key = $modelClass::tableName() . ':a:' . (is_array($pk) ? implode('-', $pk) : $pk); // TODO escape PK glue // get attributes - $row = $db->executeCommand('HGETALL', array($key)); -// TODO check for empty list if key does not exist - if ($row !== false && !$this->asArray) { + $data = $db->executeCommand('HGETALL', array($key)); + if ($data === array()) { + return null; + } + $row = array(); + for($i=0;$iasArray) { /** @var $class ActiveRecord */ $class = $this->modelClass; $model = $class::create($row); @@ -147,7 +169,7 @@ class ActiveQuery extends \yii\base\Component } return $model; } else { - return $row === false ? null : $row; + return $row; } } diff --git a/framework/db/redis/ActiveRecord.php b/framework/db/redis/ActiveRecord.php index af78331..d3faf21 100644 --- a/framework/db/redis/ActiveRecord.php +++ b/framework/db/redis/ActiveRecord.php @@ -10,7 +10,12 @@ namespace yii\db\redis; +use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; use yii\base\NotSupportedException; +use yii\base\UnknownMethodException; +use yii\db\Exception; +use yii\db\TableSchema; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. @@ -23,16 +28,6 @@ use yii\base\NotSupportedException; abstract class ActiveRecord extends \yii\db\ActiveRecord { /** - * Returns the list of all attribute names of the model. - * The default implementation will return all column names of the table associated with this AR class. - * @return array list of attribute names. - */ - public function attributes() // TODO: refactor should be abstract in an ActiveRecord base class - { - return array(); - } - - /** * 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. @@ -119,7 +114,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord */ public static function getTableSchema() { - throw new NotSupportedException('getTableSchema() is not supported by redis ActiveRecord as there is no schema in redis DB. Schema is defined by AR class itself'); + throw new InvalidConfigException(__CLASS__.'::getTableSchema() needs to be overridden in subclasses and return a TableSchema.'); } /** @@ -168,16 +163,17 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord $db = static::getDb(); $values = $this->getDirtyAttributes($attributes); $pk = array(); - if ($values === array()) { +// if ($values === array()) { foreach ($this->primaryKey() as $key) { $pk[$key] = $values[$key] = $this->getAttribute($key); if ($pk[$key] === null) { - $pk[$key] = $db->executeCommand('INCR', array(static::tableName() . ':s:', $key)); + $pk[$key] = $values[$key] = $db->executeCommand('INCR', array(static::tableName() . ':s:' . $key)); + $this->setAttribute($key, $values[$key]); } } - } +// } // save pk in a findall pool - $db->executeCommand('RPUSH', array(static::tableName(), $pk)); + $db->executeCommand('RPUSH', array(static::tableName(), implode('-', $pk))); // TODO escape PK glue $key = static::tableName() . ':a:' . implode('-', $pk); // TODO escape PK glue // save attributes @@ -252,12 +248,15 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord */ public static function updateAllCounters($counters, $condition = '', $params = array()) { + if (is_array($condition) && !isset($condition[0])) { // TODO do this in all *All methods + $condition = array($condition); + } $db = static::getDb(); if ($condition==='') { $condition = $db->executeCommand('LRANGE', array(static::tableName(), 0, -1)); } $n=0; - foreach($condition as $pk) { + foreach($condition as $pk) { // TODO allow multiple pks as condition $key = static::tableName() . ':a:' . (is_array($pk) ? implode('-', $pk) : $pk); // TODO escape PK glue foreach($counters as $attribute => $value) { $db->executeCommand('HINCRBY', array($key, $attribute, $value)); @@ -297,28 +296,269 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord $pk = implode('-', $pk); } $db->executeCommand('LREM', array(static::tableName(), 0, $pk)); // TODO escape PK glue - $attributeKeys[] = static::tableName() . ':' . $pk . ':a'; // TODO escape PK glue + $attributeKeys[] = static::tableName() . ':a:' . $pk; // TODO escape PK glue } return $db->executeCommand('DEL', $attributeKeys); } /** - * Returns the primary key name(s) for this AR class. - * The default implementation will return the primary key(s) as declared - * in the DB table that is associated with this AR class. + * Declares a `has-one` relation. + * The declaration is returned in terms of an [[ActiveRelation]] instance + * through which the related record can be queried and retrieved back. + * + * A `has-one` relation means that there is at most one related record matching + * the criteria set by this relation, e.g., a customer has one country. + * + * For example, to declare the `country` relation for `Customer` class, we can write + * the following code in the `Customer` class: + * + * ~~~ + * public function getCountry() + * { + * return $this->hasOne('Country', array('id' => 'country_id')); + * } + * ~~~ + * + * Note that in the above, the 'id' key in the `$link` parameter refers to an attribute name + * in the related class `Country`, while the 'country_id' value refers to an attribute name + * in the current AR class. + * + * Call methods declared in [[ActiveRelation]] to further customize the relation. + * + * @param string $class the class name of the related record + * @param array $link the primary-foreign key constraint. The keys of the array refer to + * the columns in the table associated with the `$class` model, while the values of the + * array refer to the corresponding columns in the table associated with this AR class. + * @return ActiveRelation the relation object. + */ + public function hasOne($class, $link) + { + return new ActiveRelation(array( + 'modelClass' => $this->getNamespacedClass($class), + 'primaryModel' => $this, + 'link' => $link, + 'multiple' => false, + )); + } + + /** + * Declares a `has-many` relation. + * The declaration is returned in terms of an [[ActiveRelation]] instance + * through which the related record can be queried and retrieved back. + * + * A `has-many` relation means that there are multiple related records matching + * the criteria set by this relation, e.g., a customer has many orders. + * + * For example, to declare the `orders` relation for `Customer` class, we can write + * the following code in the `Customer` class: + * + * ~~~ + * public function getOrders() + * { + * return $this->hasMany('Order', array('customer_id' => 'id')); + * } + * ~~~ + * + * Note that in the above, the 'customer_id' key in the `$link` parameter refers to + * an attribute name in the related class `Order`, while the 'id' value refers to + * an attribute name in the current AR class. + * + * @param string $class the class name of the related record + * @param array $link the primary-foreign key constraint. The keys of the array refer to + * the columns in the table associated with the `$class` model, while the values of the + * array refer to the corresponding columns in the table associated with this AR class. + * @return ActiveRelation the relation object. + */ + public function hasMany($class, $link) + { + return new ActiveRelation(array( + 'modelClass' => $this->getNamespacedClass($class), + 'primaryModel' => $this, + 'link' => $link, + 'multiple' => true, + )); + } + + /** + * Returns the relation object with the specified name. + * A relation is defined by a getter method which returns an [[ActiveRelation]] object. + * It can be declared in either the Active Record class itself or one of its behaviors. + * @param string $name the relation name + * @return ActiveRelation the relation object + * @throws InvalidParamException if the named relation does not exist. + */ + public function getRelation($name) + { + $getter = 'get' . $name; + try { + $relation = $this->$getter(); + if ($relation instanceof ActiveRelation) { + return $relation; + } + } catch (UnknownMethodException $e) { + } + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); + } + + /** + * Establishes the relationship between two models. + * + * The relationship is established by setting the foreign key value(s) in one model + * to be the corresponding primary key value(s) in the other model. + * The model with the foreign key will be saved into database without performing validation. * - * If the DB table does not declare any primary key, you should override - * this method to return the attributes that you want to use as primary keys - * for this AR class. + * If the relationship involves a pivot table, a new row will be inserted into the + * pivot table which contains the primary key values from both models. + * + * Note that this method requires that the primary key value is not null. + * + * @param string $name the name of the relationship + * @param ActiveRecord $model the model to be linked with the current one. + * @param array $extraColumns additional column values to be saved into the pivot table. + * This parameter is only meaningful for a relationship involving a pivot table + * (i.e., a relation set with `[[ActiveRelation::via()]]` or `[[ActiveRelation::viaTable()]]`.) + * @throws InvalidCallException if the method is unable to link two models. + */ + public function link($name, $model, $extraColumns = array()) + { + // TODO + $relation = $this->getRelation($name); + + if ($relation->via !== null) { + if (is_array($relation->via)) { + /** @var $viaRelation ActiveRelation */ + list($viaName, $viaRelation) = $relation->via; + /** @var $viaClass ActiveRecord */ + $viaClass = $viaRelation->modelClass; + $viaTable = $viaClass::tableName(); + // unset $viaName so that it can be reloaded to reflect the change + unset($this->_related[strtolower($viaName)]); + } else { + $viaRelation = $relation->via; + $viaTable = reset($relation->via->from); + } + $columns = array(); + foreach ($viaRelation->link as $a => $b) { + $columns[$a] = $this->$b; + } + foreach ($relation->link as $a => $b) { + $columns[$b] = $model->$a; + } + foreach ($extraColumns as $k => $v) { + $columns[$k] = $v; + } + static::getDb()->createCommand() + ->insert($viaTable, $columns)->execute(); + } else { + $p1 = $model->isPrimaryKey(array_keys($relation->link)); + $p2 = $this->isPrimaryKey(array_values($relation->link)); + if ($p1 && $p2) { + if ($this->getIsNewRecord() && $model->getIsNewRecord()) { + throw new InvalidCallException('Unable to link models: both models are newly created.'); + } elseif ($this->getIsNewRecord()) { + $this->bindModels(array_flip($relation->link), $this, $model); + } else { + $this->bindModels($relation->link, $model, $this); + } + } elseif ($p1) { + $this->bindModels(array_flip($relation->link), $this, $model); + } elseif ($p2) { + $this->bindModels($relation->link, $model, $this); + } else { + throw new InvalidCallException('Unable to link models: the link does not involve any primary key.'); + } + } + + // update lazily loaded related objects + if (!$relation->multiple) { + $this->_related[$name] = $model; + } elseif (isset($this->_related[$name])) { + if ($relation->indexBy !== null) { + $indexBy = $relation->indexBy; + $this->_related[$name][$model->$indexBy] = $model; + } else { + $this->_related[$name][] = $model; + } + } + } + + /** + * Destroys the relationship between two models. * - * Note that an array should be returned even for a table with single primary key. + * The model with the foreign key of the relationship will be deleted if `$delete` is true. + * Otherwise, the foreign key will be set null and the model will be saved without validation. * - * @return string[] the primary keys of the associated database table. + * @param string $name the name of the relationship. + * @param ActiveRecord $model the model to be unlinked from the current one. + * @param boolean $delete whether to delete the model that contains the foreign key. + * If false, the model's foreign key will be set null and saved. + * If true, the model containing the foreign key will be deleted. + * @throws InvalidCallException if the models cannot be unlinked */ - public static function primaryKey() // TODO: refactor should be abstract in an ActiveRecord base class + public function unlink($name, $model, $delete = false) { - return array(); + // TODO + $relation = $this->getRelation($name); + + if ($relation->via !== null) { + if (is_array($relation->via)) { + /** @var $viaRelation ActiveRelation */ + list($viaName, $viaRelation) = $relation->via; + /** @var $viaClass ActiveRecord */ + $viaClass = $viaRelation->modelClass; + $viaTable = $viaClass::tableName(); + unset($this->_related[strtolower($viaName)]); + } else { + $viaRelation = $relation->via; + $viaTable = reset($relation->via->from); + } + $columns = array(); + foreach ($viaRelation->link as $a => $b) { + $columns[$a] = $this->$b; + } + foreach ($relation->link as $a => $b) { + $columns[$b] = $model->$a; + } + $command = static::getDb()->createCommand(); + if ($delete) { + $command->delete($viaTable, $columns)->execute(); + } else { + $nulls = array(); + foreach (array_keys($columns) as $a) { + $nulls[$a] = null; + } + $command->update($viaTable, $nulls, $columns)->execute(); + } + } else { + $p1 = $model->isPrimaryKey(array_keys($relation->link)); + $p2 = $this->isPrimaryKey(array_values($relation->link)); + if ($p1 && $p2 || $p2) { + foreach ($relation->link as $a => $b) { + $model->$a = null; + } + $delete ? $model->delete() : $model->save(false); + } elseif ($p1) { + foreach ($relation->link as $b) { + $this->$b = null; + } + $delete ? $this->delete() : $this->save(false); + } else { + throw new InvalidCallException('Unable to unlink models: the link does not involve any primary key.'); + } + } + + if (!$relation->multiple) { + unset($this->_related[$name]); + } elseif (isset($this->_related[$name])) { + /** @var $b ActiveRecord */ + foreach ($this->_related[$name] as $a => $b) { + if ($model->getPrimaryKey() == $b->getPrimaryKey()) { + unset($this->_related[$name][$a]); + } + } + } } + // TODO implement link and unlink } diff --git a/framework/db/redis/ActiveRelation.php b/framework/db/redis/ActiveRelation.php index d3d5c5a..e01f3a4 100644 --- a/framework/db/redis/ActiveRelation.php +++ b/framework/db/redis/ActiveRelation.php @@ -10,6 +10,8 @@ namespace yii\db\redis; +use yii\base\NotSupportedException; + /** * ActiveRecord is the base class for classes representing relational data in terms of objects. * @@ -17,7 +19,231 @@ namespace yii\db\redis; * @author Carsten Brandt * @since 2.0 */ -class ActiveRelation extends \yii\db\ActiveRelation +class ActiveRelation extends \yii\db\redis\ActiveQuery { - // TODO implement + /** + * @var boolean whether this relation should populate all query results into AR instances. + * If false, only the first row of the results will be retrieved. + */ + public $multiple; + /** + * @var ActiveRecord the primary model that this relation is associated with. + * This is used only in lazy loading with dynamic query options. + */ + public $primaryModel; + /** + * @var array the columns of the primary and foreign tables that establish the relation. + * The array keys must be columns of the table for this relation, and the array values + * must be the corresponding columns from the primary table. + * Do not prefix or quote the column names as this will be done automatically by Yii. + */ + public $link; + /** + * @var array|ActiveRelation the query associated with the pivot table. Please call [[via()]] + * or [[viaTable()]] to set this property instead of directly setting it. + */ + public $via; + + /** + * Specifies the relation associated with the pivot table. + * @param string $relationName the relation name. This refers to a relation declared in [[primaryModel]]. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. + * Its signature should be `function($query)`, where `$query` is the query to be customized. + * @return ActiveRelation the relation object itself. + */ + public function via($relationName, $callable = null) + { + $relation = $this->primaryModel->getRelation($relationName); + $this->via = array($relationName, $relation); + if ($callable !== null) { + call_user_func($callable, $relation); + } + return $this; + } + + /** + * Specifies the pivot table. + * @param string $tableName the name of the pivot table. + * @param array $link the link between the pivot table and the table associated with [[primaryModel]]. + * The keys of the array represent the columns in the pivot table, and the values represent the columns + * in the [[primaryModel]] table. + * @param callable $callable a PHP callback for customizing the relation associated with the pivot table. + * Its signature should be `function($query)`, where `$query` is the query to be customized. + * @return ActiveRelation + * / + public function viaTable($tableName, $link, $callable = null) + { + $relation = new ActiveRelation(array( + 'modelClass' => get_class($this->primaryModel), + 'from' => array($tableName), + 'link' => $link, + 'multiple' => true, + 'asArray' => true, + )); + $this->via = $relation; + if ($callable !== null) { + call_user_func($callable, $relation); + } + return $this; + }*/ + + /** + * Finds the related records and populates them into the primary models. + * This method is internally by [[ActiveQuery]]. Do not call it directly. + * @param string $name the relation name + * @param array $primaryModels primary models + * @return array the related models + * @throws InvalidConfigException + */ + public function findWith($name, &$primaryModels) + { + if (!is_array($this->link)) { + throw new InvalidConfigException('Invalid link: it must be an array of key-value pairs.'); + } + + if ($this->via instanceof self) { + // TODO + // via pivot table + /** @var $viaQuery ActiveRelation */ + $viaQuery = $this->via; + $viaModels = $viaQuery->findPivotRows($primaryModels); + $this->filterByModels($viaModels); + } elseif (is_array($this->via)) { + // TODO + // via relation + /** @var $viaQuery ActiveRelation */ + list($viaName, $viaQuery) = $this->via; + $viaQuery->primaryModel = null; + $viaModels = $viaQuery->findWith($viaName, $primaryModels); + $this->filterByModels($viaModels); + } else { + $this->filterByModels($primaryModels); + } + + if (count($primaryModels) === 1 && !$this->multiple) { + $model = $this->one(); + foreach ($primaryModels as $i => $primaryModel) { + if ($primaryModel instanceof ActiveRecord) { + $primaryModel->populateRelation($name, $model); + } else { + $primaryModels[$i][$name] = $model; + } + } + return array($model); + } else { + $models = $this->all(); + if (isset($viaModels, $viaQuery)) { + $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery->link); + } else { + $buckets = $this->buildBuckets($models, $this->link); + } + + $link = array_values(isset($viaQuery) ? $viaQuery->link : $this->link); + foreach ($primaryModels as $i => $primaryModel) { + $key = $this->getModelKey($primaryModel, $link); + $value = isset($buckets[$key]) ? $buckets[$key] : ($this->multiple ? array() : null); + if ($primaryModel instanceof ActiveRecord) { + $primaryModel->populateRelation($name, $value); + } else { + $primaryModels[$i][$name] = $value; + } + } + return $models; + } + } + + /** + * @param array $models + * @param array $link + * @param array $viaModels + * @param array $viaLink + * @return array + */ + private function buildBuckets($models, $link, $viaModels = null, $viaLink = null) + { + $buckets = array(); + $linkKeys = array_keys($link); + foreach ($models as $i => $model) { + $key = $this->getModelKey($model, $linkKeys); + if ($this->indexBy !== null) { + $buckets[$key][$i] = $model; + } else { + $buckets[$key][] = $model; + } + } + + if ($viaModels !== null) { + $viaBuckets = array(); + $viaLinkKeys = array_keys($viaLink); + $linkValues = array_values($link); + foreach ($viaModels as $viaModel) { + $key1 = $this->getModelKey($viaModel, $viaLinkKeys); + $key2 = $this->getModelKey($viaModel, $linkValues); + if (isset($buckets[$key2])) { + foreach ($buckets[$key2] as $i => $bucket) { + if ($this->indexBy !== null) { + $viaBuckets[$key1][$i] = $bucket; + } else { + $viaBuckets[$key1][] = $bucket; + } + } + } + } + $buckets = $viaBuckets; + } + + if (!$this->multiple) { + foreach ($buckets as $i => $bucket) { + $buckets[$i] = reset($bucket); + } + } + return $buckets; + } + + /** + * @param ActiveRecord|array $model + * @param array $attributes + * @return string + */ + private function getModelKey($model, $attributes) + { + if (count($attributes) > 1) { + $key = array(); + foreach ($attributes as $attribute) { + $key[] = $model[$attribute]; + } + return serialize($key); + } else { + $attribute = reset($attributes); + return $model[$attribute]; + } + } + + + /** + * @param array $models + */ + private function filterByModels($models) + { + $attributes = array_keys($this->link); + $values = array(); + if (count($attributes) ===1) { + // single key + $attribute = reset($this->link); + foreach ($models as $model) { + $values[] = $model[$attribute]; + } + } else { + // composite keys + foreach ($models as $model) { + $v = array(); + foreach ($this->link as $attribute => $link) { + $v[$attribute] = $model[$link]; + } + $values[] = $v; + } + } + $this->primaryKeys($values); + } + } diff --git a/framework/db/redis/Connection.php b/framework/db/redis/Connection.php index 96ab288..0ea2c52 100644 --- a/framework/db/redis/Connection.php +++ b/framework/db/redis/Connection.php @@ -12,7 +12,7 @@ namespace yii\db\redis; use \yii\base\Component; use yii\base\InvalidConfigException; use \yii\db\Exception; -use yii\util\StringHelper; +use yii\helpers\StringHelper; /** * diff --git a/tests/unit/data/ar/redis/ActiveRecord.php b/tests/unit/data/ar/redis/ActiveRecord.php new file mode 100644 index 0000000..7419479 --- /dev/null +++ b/tests/unit/data/ar/redis/ActiveRecord.php @@ -0,0 +1,26 @@ + + * @since 2.0 + */ +class ActiveRecord extends \yii\db\redis\ActiveRecord +{ + public static $db; + + public static function getDb() + { + return self::$db; + } +} \ No newline at end of file diff --git a/tests/unit/data/ar/redis/Customer.php b/tests/unit/data/ar/redis/Customer.php new file mode 100644 index 0000000..9e7ea62 --- /dev/null +++ b/tests/unit/data/ar/redis/Customer.php @@ -0,0 +1,35 @@ +hasMany('Order', array('customer_id' => 'id')); + } + + public static function getTableSchema() + { + return new TableSchema(array( + 'primaryKey' => array('id'), + 'columns' => array( + 'id' => 'integer', + 'email' => 'string', + 'name' => 'string', + 'address' => 'string', + 'status' => 'integer' + ) + )); + } +} \ No newline at end of file diff --git a/tests/unit/data/ar/redis/Item.php b/tests/unit/data/ar/redis/Item.php new file mode 100644 index 0000000..6dbaa2f --- /dev/null +++ b/tests/unit/data/ar/redis/Item.php @@ -0,0 +1,25 @@ + array('id'), + 'columns' => array( + 'id' => 'integer', + 'name' => 'string', + 'category_id' => 'integer' + ) + )); + } +} \ No newline at end of file diff --git a/tests/unit/data/ar/redis/Order.php b/tests/unit/data/ar/redis/Order.php new file mode 100644 index 0000000..d97a3af --- /dev/null +++ b/tests/unit/data/ar/redis/Order.php @@ -0,0 +1,63 @@ +hasOne('Customer', array('id' => 'customer_id')); + } + + public function getOrderItems() + { + return $this->hasMany('OrderItem', array('order_id' => 'id')); + } + + public function getItems() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->via('orderItems', function($q) { + // additional query configuration + }); + } + + public function getBooks() + { + return $this->hasMany('Item', array('id' => 'item_id')) + ->via('orderItems', array('order_id' => 'id')); + //->where(array('category_id' => 1)); + } + + public function beforeSave($insert) + { + if (parent::beforeSave($insert)) { + $this->create_time = time(); + return true; + } else { + return false; + } + } + + + public static function getTableSchema() + { + return new TableSchema(array( + 'primaryKey' => array('id'), + 'columns' => array( + 'id' => 'integer', + 'customer_id' => 'integer', + 'create_time' => 'integer', + 'total' => 'decimal', + ) + )); + } + +} \ No newline at end of file diff --git a/tests/unit/data/ar/redis/OrderItem.php b/tests/unit/data/ar/redis/OrderItem.php new file mode 100644 index 0000000..257b9b0 --- /dev/null +++ b/tests/unit/data/ar/redis/OrderItem.php @@ -0,0 +1,36 @@ +hasOne('Order', array('id' => 'order_id')); + } + + public function getItem() + { + return $this->hasOne('Item', array('id' => 'item_id')); + } + + public static function getTableSchema() + { + return new TableSchema(array( + 'primaryKey' => array('order_id', 'item_id'), + 'columns' => array( + 'order_id' => 'integer', + 'item_id' => 'integer', + 'quantity' => 'integer', + 'subtotal' => 'decimal', + ) + )); + } +} \ No newline at end of file diff --git a/tests/unit/framework/db/redis/ActiveRecordTest.php b/tests/unit/framework/db/redis/ActiveRecordTest.php new file mode 100644 index 0000000..74c5734 --- /dev/null +++ b/tests/unit/framework/db/redis/ActiveRecordTest.php @@ -0,0 +1,402 @@ +getConnection(); + + $customer = new Customer(); + $customer->setAttributes(array('email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1), false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(array('email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1), false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(array('email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2), false); + $customer->save(false); + +// INSERT INTO tbl_category (name) VALUES ('Books'); +// INSERT INTO tbl_category (name) VALUES ('Movies'); + + $item = new Item(); + $item->setAttributes(array('name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1), false); + $item->save(false); + $item = new Item(); + $item->setAttributes(array('name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1), false); + $item->save(false); + $item = new Item(); + $item->setAttributes(array('name' => 'Ice Age', 'category_id' => 2), false); + $item->save(false); + $item = new Item(); + $item->setAttributes(array('name' => 'Toy Story', 'category_id' => 2), false); + $item->save(false); + $item = new Item(); + $item->setAttributes(array('name' => 'Cars', 'category_id' => 2), false); + $item->save(false); + + $order = new Order(); + $order->setAttributes(array('customer_id' => 1, 'create_time' => 1325282384, 'total' => 110.0), false); + $order->save(false); + $order = new Order(); + $order->setAttributes(array('customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0), false); + $order->save(false); + $order = new Order(); + $order->setAttributes(array('customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0), false); + $order->save(false); + + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0), false); + $orderItem->save(false); + $orderItem = new OrderItem(); + $orderItem->setAttributes(array('order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0), false); + $orderItem->save(false); + + parent::setUp(); + } + + public function testFind() + { + // find one + $result = Customer::find(); + $this->assertTrue($result instanceof ActiveQuery); + $customer = $result->one(); + $this->assertTrue($customer instanceof Customer); + + // find all + $customers = Customer::find()->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers[0] instanceof Customer); + $this->assertTrue($customers[1] instanceof Customer); + $this->assertTrue($customers[2] instanceof Customer); + + // find by a single primary key + $customer = Customer::find(2); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + + // find by column values + $customer = Customer::find(array('id' => 2)); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + $customer = Customer::find(array('id' => 5)); + $this->assertNull($customer); + + // find by attributes +/* $customer = Customer::find()->where(array('name' => 'user2'))->one(); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals(2, $customer->id);*/ + + // find custom column +/* $customer = Customer::find()->select(array('*', '(status*2) AS status2')) + ->where(array('name' => 'user3'))->one(); + $this->assertEquals(3, $customer->id); + $this->assertEquals(4, $customer->status2);*/ + + // find count, sum, average, min, max, scalar + $this->assertEquals(3, Customer::find()->count()); +/* $this->assertEquals(2, Customer::find()->where('id=1 OR id=2')->count()); + $this->assertEquals(6, Customer::find()->sum('id')); + $this->assertEquals(2, Customer::find()->average('id')); + $this->assertEquals(1, Customer::find()->min('id')); + $this->assertEquals(3, Customer::find()->max('id')); + $this->assertEquals(3, Customer::find()->select('COUNT(*)')->scalar());*/ + + // scope +// $this->assertEquals(2, Customer::find()->active()->count()); + + // asArray + $customer = Customer::find()->primaryKeys(array(2))->asArray()->one(); + $this->assertEquals(array( + 'id' => '2', + 'email' => 'user2@example.com', + 'name' => 'user2', + 'address' => 'address2', + 'status' => '1', + ), $customer); + + // indexBy + $customers = Customer::find()->indexBy('name')->all(); + $this->assertEquals(3, count($customers)); + $this->assertTrue($customers['user1'] instanceof Customer); + $this->assertTrue($customers['user2'] instanceof Customer); + $this->assertTrue($customers['user3'] instanceof Customer); + } + + public function testFindLazy() + { + /** @var $customer Customer */ + $customer = Customer::find(2); + $orders = $customer->orders; + $this->assertEquals(2, count($orders)); + + $orders = $customer->getOrders()->primaryKeys(array(3))->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(3, $orders[0]->id); + } + + public function testFindEager() + { + $customers = Customer::find()->with('orders')->all(); + $this->assertEquals(3, count($customers)); + $this->assertEquals(1, count($customers[0]->orders)); + $this->assertEquals(2, count($customers[1]->orders)); + } + + public function testFindLazyVia() + { + /** @var $order Order */ + $order = Order::find(1); + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + + $order = Order::find(1); + $order->id = 100; + $this->assertEquals(array(), $order->items); + } + + public function testFindEagerViaRelation() + { + $orders = Order::find()->with('items')->all(); + $this->assertEquals(3, count($orders)); + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + +/* public function testFindLazyViaTable() + { + /** @var $order Order * / + $order = Order::find(1); + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->books)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + + $order = Order::find(2); + $this->assertEquals(2, $order->id); + $this->assertEquals(0, count($order->books)); + } + + public function testFindEagerViaTable() + { + $orders = Order::find()->with('books')->all(); + $this->assertEquals(3, count($orders)); + + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->books)); + $this->assertEquals(1, $order->books[0]->id); + $this->assertEquals(2, $order->books[1]->id); + + $order = $orders[1]; + $this->assertEquals(2, $order->id); + $this->assertEquals(0, count($order->books)); + + $order = $orders[2]; + $this->assertEquals(3, $order->id); + $this->assertEquals(1, count($order->books)); + $this->assertEquals(2, $order->books[0]->id); + }*/ + + public function testFindNestedRelation() + { + $customers = Customer::find()->with('orders', 'orders.items')->all(); + $this->assertEquals(3, count($customers)); + $this->assertEquals(1, count($customers[0]->orders)); + $this->assertEquals(2, count($customers[1]->orders)); + $this->assertEquals(0, count($customers[2]->orders)); + $this->assertEquals(2, count($customers[0]->orders[0]->items)); + $this->assertEquals(3, count($customers[1]->orders[0]->items)); + $this->assertEquals(1, count($customers[1]->orders[1]->items)); + } + + public function testLink() + { + $customer = Customer::find(2); + $this->assertEquals(2, count($customer->orders)); + + // has many + $order = new Order; + $order->total = 100; + $this->assertTrue($order->isNewRecord); + $customer->link('orders', $order); + $this->assertEquals(3, count($customer->orders)); + $this->assertFalse($order->isNewRecord); + $this->assertEquals(3, count($customer->getOrders()->all())); + $this->assertEquals(2, $order->customer_id); + + // belongs to + $order = new Order; + $order->total = 100; + $this->assertTrue($order->isNewRecord); + $customer = Customer::find(1); + $this->assertNull($order->customer); + $order->link('customer', $customer); + $this->assertFalse($order->isNewRecord); + $this->assertEquals(1, $order->customer_id); + $this->assertEquals(1, $order->customer->id); + + // via table + $order = Order::find(2); + $this->assertEquals(0, count($order->books)); + $orderItem = OrderItem::find(array('order_id' => 2, 'item_id' => 1)); + $this->assertNull($orderItem); + $item = Item::find(1); + $order->link('books', $item, array('quantity' => 10, 'subtotal' => 100)); + $this->assertEquals(1, count($order->books)); + $orderItem = OrderItem::find(array('order_id' => 2, 'item_id' => 1)); + $this->assertTrue($orderItem instanceof OrderItem); + $this->assertEquals(10, $orderItem->quantity); + $this->assertEquals(100, $orderItem->subtotal); + + // via model + $order = Order::find(1); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(2, count($order->orderItems)); + $orderItem = OrderItem::find(array('order_id' => 1, 'item_id' => 3)); + $this->assertNull($orderItem); + $item = Item::find(3); + $order->link('items', $item, array('quantity' => 10, 'subtotal' => 100)); + $this->assertEquals(3, count($order->items)); + $this->assertEquals(3, count($order->orderItems)); + $orderItem = OrderItem::find(array('order_id' => 1, 'item_id' => 3)); + $this->assertTrue($orderItem instanceof OrderItem); + $this->assertEquals(10, $orderItem->quantity); + $this->assertEquals(100, $orderItem->subtotal); + } + + public function testUnlink() + { + // has many + $customer = Customer::find(2); + $this->assertEquals(2, count($customer->orders)); + $customer->unlink('orders', $customer->orders[1], true); + $this->assertEquals(1, count($customer->orders)); + $this->assertNull(Order::find(3)); + + // via model + $order = Order::find(2); + $this->assertEquals(3, count($order->items)); + $this->assertEquals(3, count($order->orderItems)); + $order->unlink('items', $order->items[2], true); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(2, count($order->orderItems)); + + // via table + $order = Order::find(1); + $this->assertEquals(2, count($order->books)); + $order->unlink('books', $order->books[1], true); + $this->assertEquals(1, count($order->books)); + $this->assertEquals(1, count($order->orderItems)); + } + + public function testInsert() + { + $customer = new Customer; + $customer->email = 'user4@example.com'; + $customer->name = 'user4'; + $customer->address = 'address4'; + + $this->assertNull($customer->id); + $this->assertTrue($customer->isNewRecord); + + $customer->save(); + + $this->assertEquals(4, $customer->id); + $this->assertFalse($customer->isNewRecord); + } + + public function testUpdate() + { + // save + $customer = Customer::find(2); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + $this->assertFalse($customer->isNewRecord); + $customer->name = 'user2x'; + $customer->save(); + $this->assertEquals('user2x', $customer->name); + $this->assertFalse($customer->isNewRecord); + $customer2 = Customer::find(2); + $this->assertEquals('user2x', $customer2->name); + + // updateCounters + $pk = array('order_id' => 2, 'item_id' => 4); + $orderItem = OrderItem::find($pk); + $this->assertEquals(1, $orderItem->quantity); + $ret = $orderItem->updateCounters(array('quantity' => -1)); + $this->assertTrue($ret); + $this->assertEquals(0, $orderItem->quantity); + $orderItem = OrderItem::find($pk); + $this->assertEquals(0, $orderItem->quantity); + + // updateAll + $customer = Customer::find(3); + $this->assertEquals('user3', $customer->name); + $ret = Customer::updateAll(array( + 'name' => 'temp', + ), array('id' => 3)); + $this->assertEquals(1, $ret); + $customer = Customer::find(3); + $this->assertEquals('temp', $customer->name); + + // updateCounters + $pk = array('order_id' => 1, 'item_id' => 2); + $orderItem = OrderItem::find($pk); + $this->assertEquals(2, $orderItem->quantity); + $ret = OrderItem::updateAllCounters(array( + 'quantity' => 3, + 'subtotal' => -10, + ), $pk); + $this->assertEquals(1, $ret); + $orderItem = OrderItem::find($pk); + $this->assertEquals(5, $orderItem->quantity); + $this->assertEquals(30, $orderItem->subtotal); + } + + public function testDelete() + { + // delete + $customer = Customer::find(2); + $this->assertTrue($customer instanceof Customer); + $this->assertEquals('user2', $customer->name); + $customer->delete(); + $customer = Customer::find(2); + $this->assertNull($customer); + + // deleteAll + $customers = Customer::find()->all(); + $this->assertEquals(2, count($customers)); + $ret = Customer::deleteAll(); + $this->assertEquals(2, $ret); + $customers = Customer::find()->all(); + $this->assertEquals(0, count($customers)); + } +} \ No newline at end of file