Browse Source

polished elasticsearch AR api, added mget and fixed AR::equals()

tags/2.0.0-alpha
Carsten Brandt 11 years ago
parent
commit
c6347d6d01
  1. 8
      framework/yii/db/ActiveRecord.php
  2. 120
      framework/yii/elasticsearch/ActiveRecord.php
  3. 30
      framework/yii/elasticsearch/Command.php
  4. 33
      tests/unit/framework/db/ActiveRecordTest.php
  5. 84
      tests/unit/framework/elasticsearch/ActiveRecordTest.php
  6. 33
      tests/unit/framework/redis/ActiveRecordTest.php

8
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();
}

120
framework/yii/elasticsearch/ActiveRecord.php

@ -25,7 +25,7 @@ use yii\helpers\StringHelper;
* @author Carsten Brandt <mail@cebe.cc>
* @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;
}

30
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

33
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

84
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);
}
}

33
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());

Loading…
Cancel
Save