From c6347d6d01dfb47bad878b1894c2dc2ba6815dce Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sat, 23 Nov 2013 15:26:00 +0100 Subject: [PATCH] polished elasticsearch AR api, added mget and fixed AR::equals() --- framework/yii/db/ActiveRecord.php | 8 +- framework/yii/elasticsearch/ActiveRecord.php | 120 +++++++++++++++++++-- framework/yii/elasticsearch/Command.php | 30 ++++++ tests/unit/framework/db/ActiveRecordTest.php | 33 ++++++ .../framework/elasticsearch/ActiveRecordTest.php | 84 ++++++++++++--- tests/unit/framework/redis/ActiveRecordTest.php | 33 ++++++ 6 files changed, 282 insertions(+), 26 deletions(-) diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php index ffae3d8..fce9010 100644 --- a/framework/yii/db/ActiveRecord.php +++ b/framework/yii/db/ActiveRecord.php @@ -132,7 +132,7 @@ class ActiveRecord extends Model * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. * - null: return a new [[ActiveQuery]] object for further query purpose. * - * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance + * @return ActiveQuery|ActiveQueryInterface|static|null When `$q` is null, a new [[ActiveQuery]] instance * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be * returned (null will be returned if there is no matching). * @throws InvalidConfigException if the AR class does not have a primary key @@ -754,7 +754,7 @@ class ActiveRecord extends Model * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] * will be raised by the corresponding methods. * - * Only the [[changedAttributes|changed attribute values]] will be inserted into database. + * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. * * If the table's primary key is auto-incremental and is null during insertion, * it will be populated with the actual value after insertion. @@ -1179,11 +1179,15 @@ class ActiveRecord extends Model /** * Returns a value indicating whether the given active record is the same as the current one. * The comparison is made by comparing the table names and the primary key values of the two active records. + * If one of the records [[isNewRecord|is new]] they are also considered not equal. * @param ActiveRecord $record record to compare to * @return boolean whether the two active records refer to the same row in the same database table. */ public function equals($record) { + if ($this->isNewRecord || $record->isNewRecord) { + return false; + } return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey(); } diff --git a/framework/yii/elasticsearch/ActiveRecord.php b/framework/yii/elasticsearch/ActiveRecord.php index e46201b..3ea130f 100644 --- a/framework/yii/elasticsearch/ActiveRecord.php +++ b/framework/yii/elasticsearch/ActiveRecord.php @@ -25,7 +25,7 @@ use yii\helpers\StringHelper; * @author Carsten Brandt * @since 2.0 */ -abstract class ActiveRecord extends \yii\db\ActiveRecord +class ActiveRecord extends \yii\db\ActiveRecord { private $_id; private $_version; @@ -48,7 +48,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord { $query = static::createQuery(); if (is_array($q)) { - if (count($q) == 1 && isset($q['primaryKey'])) { + if (count($q) == 1 && (array_key_exists('primaryKey', $q))) { return static::get($q['primaryKey']); } return $query->where($q)->one(); @@ -58,8 +58,23 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord return $query; } + /** + * Gets a record by its primary key. + * + * @param mixed $primaryKey the primaryKey value + * @param array $options options given in this parameter are passed to elasticsearch + * as request URI parameters. + * + * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) + * for more details on these options. + * @return static|null The record instance or null if it was not found. + */ + public static function get($primaryKey, $options = []) { + if ($primaryKey === null) { + return null; + } $command = static::getDb()->createCommand(); $result = $command->get(static::index(), static::type(), $primaryKey, $options); if ($result['exists']) { @@ -69,6 +84,34 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord } /** + * Gets a list of records by its primary keys. + * + * @param array $primaryKeys an array of primaryKey values + * @param array $options options given in this parameter are passed to elasticsearch + * as request URI parameters. + * + * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) + * for more details on these options. + * @return static|null The record instance or null if it was not found. + */ + + public static function mget($primaryKeys, $options = []) + { + if (empty($primaryKeys)) { + return []; + } + $command = static::getDb()->createCommand(); + $result = $command->mget(static::index(), static::type(), $primaryKeys, $options); + $models = []; + foreach($result['docs'] as $doc) { + if ($doc['exists']) { + $models[] = static::create($doc); + } + } + return $models; + } + + /** * @inheritDoc */ public static function createQuery() @@ -117,7 +160,12 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord */ public function getOldPrimaryKey($asArray = false) { - return $this->getPrimaryKey($asArray); + $id = $this->isNewRecord ? null : $this->_id; + if ($asArray) { + return ['primaryKey' => $id]; + } else { + return $this->_id; + } } /** @@ -142,12 +190,17 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); } - // TODO index and type definition + /** + * @return string the name of the index this record is stored in. + */ public static function index() { return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); } + /** + * @return string the name of the type of this record. + */ public static function type() { return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); @@ -168,9 +221,56 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord } /** - * @inheritDocs + * Inserts a document into the associated index using the attribute values of this record. + * + * This method performs the following steps in order: + * + * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation + * fails, it will skip the rest of the steps; + * 2. call [[afterValidate()]] when `$runValidation` is true. + * 3. call [[beforeSave()]]. If the method returns false, it will skip the + * rest of the steps; + * 4. insert the record into database. If this fails, it will skip the rest of the steps; + * 5. call [[afterSave()]]; + * + * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], + * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] + * will be raised by the corresponding methods. + * + * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. + * + * If the [[primaryKey|primary key]] is not set (null) during insertion, + * it will be populated with a + * [randomly generated value](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation) + * after insertion. + * + * For example, to insert a customer record: + * + * ~~~ + * $customer = new Customer; + * $customer->name = $name; + * $customer->email = $email; + * $customer->insert(); + * ~~~ + * + * @param boolean $runValidation whether to perform validation before saving the record. + * If the validation fails, the record will not be inserted into the database. + * @param array $attributes list of attributes that need to be saved. Defaults to null, + * meaning all attributes will be saved. + * @param array $options options given in this parameter are passed to elasticsearch + * as request URI parameters. These are among others: + * + * - `routing` define shard placement of this record. + * - `parent` by giving the primaryKey of another record this defines a parent-child relation + * - `timestamp` specifies the timestamp to store along with the document. Default is indexing time. + * + * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html) + * for more details on these options. + * + * By default the `op_type` is set to `create`. + * @return boolean whether the attributes are valid and the record is inserted successfully. */ - public function insert($runValidation = true, $attributes = null) + public function insert($runValidation = true, $attributes = null, $options = ['op_type' => 'create']) { if ($runValidation && !$this->validate($attributes)) { return false; @@ -182,7 +282,8 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord static::index(), static::type(), $values, - $this->getPrimaryKey() + $this->getPrimaryKey(), + $options ); if (!$response['ok']) { @@ -268,7 +369,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord } $bulk = ''; foreach((array) $condition as $pk) { - $bulk = Json::encode([ + $bulk .= Json::encode([ "delete" => [ "_id" => $pk, "_type" => static::type(), @@ -283,10 +384,9 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord $body = Json::decode($response->getBody(true)); $n=0; foreach($body['items'] as $item) { - if ($item['delete']['ok']) { + if ($item['delete']['found'] && $item['delete']['ok']) { $n++; } - // TODO might want to update the _version in update() } return $n; } diff --git a/framework/yii/elasticsearch/Command.php b/framework/yii/elasticsearch/Command.php index 36bc696..b19d763 100644 --- a/framework/yii/elasticsearch/Command.php +++ b/framework/yii/elasticsearch/Command.php @@ -116,6 +116,36 @@ class Command extends Component } /** + * gets multiple documents from the index + * + * TODO allow specifying type and index + fields + * @param $index + * @param $type + * @param $id + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html + */ + public function mget($index, $type, $ids, $options = []) + { + $httpOptions = [ + 'exceptions' => false, + ]; + $body = Json::encode(['ids' => array_values($ids)]); + $response = $this->db->http()->post( // TODO guzzle does not manage to send get request with content + $this->createUrl([$index, $type, '_mget'], $options), + null, + $body, + $httpOptions + )->send(); + if ($response->getStatusCode() == 200) { + return Json::decode($response->getBody(true)); + } else { + throw new Exception('Elasticsearch request failed.'); + } + } + + /** * gets a documents _source from the index (>=v0.90.1) * @param $index * @param $type diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 3de40dd..24e5be8 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -104,6 +104,39 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertTrue($customers['3-user3'] instanceof Customer); } + public function testRefresh() + { + $customer = new Customer(); + $this->assertFalse($customer->refresh()); + + $customer = Customer::find(1); + $customer->name = 'to be refreshed'; + $this->assertTrue($customer->refresh()); + $this->assertEquals('user1', $customer->name); + } + + public function testEquals() + { + $customerA = new Customer(); + $customerB = new Customer(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = new Customer(); + $customerB = new Item(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = Customer::find(1); + $customerB = Customer::find(2); + $this->assertFalse($customerA->equals($customerB)); + + $customerB = Customer::find(1); + $this->assertTrue($customerA->equals($customerB)); + + $customerA = Customer::find(1); + $customerB = Item::find(1); + $this->assertFalse($customerA->equals($customerB)); + } + public function testFindBySql() { // find one diff --git a/tests/unit/framework/elasticsearch/ActiveRecordTest.php b/tests/unit/framework/elasticsearch/ActiveRecordTest.php index 9ac0fdc..c164678 100644 --- a/tests/unit/framework/elasticsearch/ActiveRecordTest.php +++ b/tests/unit/framework/elasticsearch/ActiveRecordTest.php @@ -93,17 +93,6 @@ class ActiveRecordTest extends ElasticSearchTestCase // $orderItem->save(false); Customer::getDb()->createCommand()->flushIndex(); - -// for($n = 0; $n < 20; $n++) { -// $r = $db->http()->post('_count')->send(); -// $c = Json::decode($r->getBody(true)); -// if ($c['count'] != 11) { -// usleep(100000); -// } else { -// return; -// } -// } -// throw new \Exception('Unable to initialize elasticsearch data.'); } public function testFind() @@ -187,6 +176,62 @@ class ActiveRecordTest extends ElasticSearchTestCase $this->assertTrue($customers['2-user3'] instanceof Customer); } + public function testGet() + { + $this->assertInstanceOf(Customer::className(), Customer::get(1)); + $this->assertNull(Customer::get(5)); + } + + public function testMget() + { + $this->assertEquals([], Customer::mget([])); + + $records = Customer::mget([1]); + $this->assertEquals(1, count($records)); + $this->assertInstanceOf(Customer::className(), reset($records)); + + $records = Customer::mget([5]); + $this->assertEquals(0, count($records)); + + $records = Customer::mget([1,3,5]); + $this->assertEquals(2, count($records)); + $this->assertInstanceOf(Customer::className(), $records[0]); + $this->assertInstanceOf(Customer::className(), $records[1]); + } + + public function testRefresh() + { + $customer = new Customer(); + $this->assertFalse($customer->refresh()); + + $customer = Customer::get(1); + $customer->name = 'to be refreshed'; + $this->assertTrue($customer->refresh()); + $this->assertEquals('user1', $customer->name); + } + + public function testEquals() + { + $customerA = new Customer(); + $customerB = new Customer(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = new Customer(); + $customerB = new Item(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = Customer::find(1); + $customerB = Customer::find(2); + $this->assertFalse($customerA->equals($customerB)); + + $customerB = Customer::find(1); + $this->assertTrue($customerA->equals($customerB)); + + $customerA = Customer::find(1); + $customerB = Item::find(1); + $this->assertFalse($customerA->equals($customerB)); + } + public function testFindCount() { $this->assertEquals(3, Customer::find()->count()); @@ -388,6 +433,8 @@ class ActiveRecordTest extends ElasticSearchTestCase public function testInsertNoPk() { + $this->assertEquals(['primaryKey'], Customer::primaryKey()); + $customer = new Customer; $customer->email = 'user4@example.com'; $customer->name = 'user4'; @@ -435,17 +482,20 @@ class ActiveRecordTest extends ElasticSearchTestCase // updateAll $customer = Customer::find(3); $this->assertEquals('user3', $customer->name); - $ret = Customer::updateAll(array( + $ret = Customer::updateAll([ 'name' => 'temp', - ), array('id' => 3)); + ], ['id' => 3]); $this->assertEquals(1, $ret); $customer = Customer::find(3); $this->assertEquals('temp', $customer->name); + + $ret = Customer::updateAll(['name' => 'temp']); + $this->assertEquals(0, $ret); } public function testUpdatePk() { - $this->setExpectedException('yii\base\NotSupportedException'); + $this->setExpectedException('yii\base\InvalidCallException'); $pk = array('primaryKey' => 2); $orderItem = Order::find($pk); @@ -472,7 +522,13 @@ class ActiveRecordTest extends ElasticSearchTestCase $this->assertEquals(2, count($customers)); $ret = Customer::deleteAll([1,2,3]); $this->assertEquals(2, $ret); + + Customer::getDb()->createCommand()->flushIndex('customers'); + $customers = Customer::find()->all(); $this->assertEquals(0, count($customers)); + + $ret = Customer::deleteAll(); + $this->assertEquals(0, $ret); } } \ No newline at end of file diff --git a/tests/unit/framework/redis/ActiveRecordTest.php b/tests/unit/framework/redis/ActiveRecordTest.php index 31907f7..384ddd9 100644 --- a/tests/unit/framework/redis/ActiveRecordTest.php +++ b/tests/unit/framework/redis/ActiveRecordTest.php @@ -161,6 +161,39 @@ class ActiveRecordTest extends RedisTestCase $this->assertTrue($customers['3-user3'] instanceof Customer); } + public function testRefresh() + { + $customer = new Customer(); + $this->assertFalse($customer->refresh()); + + $customer = Customer::find(1); + $customer->name = 'to be refreshed'; + $this->assertTrue($customer->refresh()); + $this->assertEquals('user1', $customer->name); + } + + public function testEquals() + { + $customerA = new Customer(); + $customerB = new Customer(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = new Customer(); + $customerB = new Item(); + $this->assertFalse($customerA->equals($customerB)); + + $customerA = Customer::find(1); + $customerB = Customer::find(2); + $this->assertFalse($customerA->equals($customerB)); + + $customerB = Customer::find(1); + $this->assertTrue($customerA->equals($customerB)); + + $customerA = Customer::find(1); + $customerB = Item::find(1); + $this->assertFalse($customerA->equals($customerB)); + } + public function testFindCount() { $this->assertEquals(3, Customer::find()->count());