Commit c6347d6d by Carsten Brandt

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

parent 426223af
...@@ -132,7 +132,7 @@ class ActiveRecord extends Model ...@@ -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. * - 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. * - 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 * 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). * returned (null will be returned if there is no matching).
* @throws InvalidConfigException if the AR class does not have a primary key * @throws InvalidConfigException if the AR class does not have a primary key
...@@ -754,7 +754,7 @@ class ActiveRecord extends Model ...@@ -754,7 +754,7 @@ class ActiveRecord extends Model
* [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
* will be raised by the corresponding methods. * 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, * If the table's primary key is auto-incremental and is null during insertion,
* it will be populated with the actual value after insertion. * it will be populated with the actual value after insertion.
...@@ -1179,11 +1179,15 @@ class ActiveRecord extends Model ...@@ -1179,11 +1179,15 @@ class ActiveRecord extends Model
/** /**
* Returns a value indicating whether the given active record is the same as the current one. * 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. * 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 * @param ActiveRecord $record record to compare to
* @return boolean whether the two active records refer to the same row in the same database table. * @return boolean whether the two active records refer to the same row in the same database table.
*/ */
public function equals($record) public function equals($record)
{ {
if ($this->isNewRecord || $record->isNewRecord) {
return false;
}
return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey(); return $this->tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
} }
......
...@@ -25,7 +25,7 @@ use yii\helpers\StringHelper; ...@@ -25,7 +25,7 @@ use yii\helpers\StringHelper;
* @author Carsten Brandt <mail@cebe.cc> * @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
abstract class ActiveRecord extends \yii\db\ActiveRecord class ActiveRecord extends \yii\db\ActiveRecord
{ {
private $_id; private $_id;
private $_version; private $_version;
...@@ -48,7 +48,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord ...@@ -48,7 +48,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord
{ {
$query = static::createQuery(); $query = static::createQuery();
if (is_array($q)) { 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 static::get($q['primaryKey']);
} }
return $query->where($q)->one(); return $query->where($q)->one();
...@@ -58,8 +58,23 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord ...@@ -58,8 +58,23 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord
return $query; 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 = []) public static function get($primaryKey, $options = [])
{ {
if ($primaryKey === null) {
return null;
}
$command = static::getDb()->createCommand(); $command = static::getDb()->createCommand();
$result = $command->get(static::index(), static::type(), $primaryKey, $options); $result = $command->get(static::index(), static::type(), $primaryKey, $options);
if ($result['exists']) { if ($result['exists']) {
...@@ -69,6 +84,34 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord ...@@ -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 * @inheritDoc
*/ */
public static function createQuery() public static function createQuery()
...@@ -117,7 +160,12 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord ...@@ -117,7 +160,12 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord
*/ */
public function getOldPrimaryKey($asArray = false) 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 ...@@ -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.'); 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() public static function index()
{ {
return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-'));
} }
/**
* @return string the name of the type of this record.
*/
public static function type() public static function type()
{ {
return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
...@@ -168,9 +221,56 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord ...@@ -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)) { if ($runValidation && !$this->validate($attributes)) {
return false; return false;
...@@ -182,7 +282,8 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord ...@@ -182,7 +282,8 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord
static::index(), static::index(),
static::type(), static::type(),
$values, $values,
$this->getPrimaryKey() $this->getPrimaryKey(),
$options
); );
if (!$response['ok']) { if (!$response['ok']) {
...@@ -268,7 +369,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord ...@@ -268,7 +369,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord
} }
$bulk = ''; $bulk = '';
foreach((array) $condition as $pk) { foreach((array) $condition as $pk) {
$bulk = Json::encode([ $bulk .= Json::encode([
"delete" => [ "delete" => [
"_id" => $pk, "_id" => $pk,
"_type" => static::type(), "_type" => static::type(),
...@@ -283,10 +384,9 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord ...@@ -283,10 +384,9 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord
$body = Json::decode($response->getBody(true)); $body = Json::decode($response->getBody(true));
$n=0; $n=0;
foreach($body['items'] as $item) { foreach($body['items'] as $item) {
if ($item['delete']['ok']) { if ($item['delete']['found'] && $item['delete']['ok']) {
$n++; $n++;
} }
// TODO might want to update the _version in update()
} }
return $n; return $n;
} }
......
...@@ -116,6 +116,36 @@ class Command extends Component ...@@ -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) * gets a documents _source from the index (>=v0.90.1)
* @param $index * @param $index
* @param $type * @param $type
......
...@@ -104,6 +104,39 @@ class ActiveRecordTest extends DatabaseTestCase ...@@ -104,6 +104,39 @@ class ActiveRecordTest extends DatabaseTestCase
$this->assertTrue($customers['3-user3'] instanceof Customer); $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() public function testFindBySql()
{ {
// find one // find one
......
...@@ -93,17 +93,6 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -93,17 +93,6 @@ class ActiveRecordTest extends ElasticSearchTestCase
// $orderItem->save(false); // $orderItem->save(false);
Customer::getDb()->createCommand()->flushIndex(); 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() public function testFind()
...@@ -187,6 +176,62 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -187,6 +176,62 @@ class ActiveRecordTest extends ElasticSearchTestCase
$this->assertTrue($customers['2-user3'] instanceof Customer); $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() public function testFindCount()
{ {
$this->assertEquals(3, Customer::find()->count()); $this->assertEquals(3, Customer::find()->count());
...@@ -388,6 +433,8 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -388,6 +433,8 @@ class ActiveRecordTest extends ElasticSearchTestCase
public function testInsertNoPk() public function testInsertNoPk()
{ {
$this->assertEquals(['primaryKey'], Customer::primaryKey());
$customer = new Customer; $customer = new Customer;
$customer->email = 'user4@example.com'; $customer->email = 'user4@example.com';
$customer->name = 'user4'; $customer->name = 'user4';
...@@ -435,17 +482,20 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -435,17 +482,20 @@ class ActiveRecordTest extends ElasticSearchTestCase
// updateAll // updateAll
$customer = Customer::find(3); $customer = Customer::find(3);
$this->assertEquals('user3', $customer->name); $this->assertEquals('user3', $customer->name);
$ret = Customer::updateAll(array( $ret = Customer::updateAll([
'name' => 'temp', 'name' => 'temp',
), array('id' => 3)); ], ['id' => 3]);
$this->assertEquals(1, $ret); $this->assertEquals(1, $ret);
$customer = Customer::find(3); $customer = Customer::find(3);
$this->assertEquals('temp', $customer->name); $this->assertEquals('temp', $customer->name);
$ret = Customer::updateAll(['name' => 'temp']);
$this->assertEquals(0, $ret);
} }
public function testUpdatePk() public function testUpdatePk()
{ {
$this->setExpectedException('yii\base\NotSupportedException'); $this->setExpectedException('yii\base\InvalidCallException');
$pk = array('primaryKey' => 2); $pk = array('primaryKey' => 2);
$orderItem = Order::find($pk); $orderItem = Order::find($pk);
...@@ -472,7 +522,13 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -472,7 +522,13 @@ class ActiveRecordTest extends ElasticSearchTestCase
$this->assertEquals(2, count($customers)); $this->assertEquals(2, count($customers));
$ret = Customer::deleteAll([1,2,3]); $ret = Customer::deleteAll([1,2,3]);
$this->assertEquals(2, $ret); $this->assertEquals(2, $ret);
Customer::getDb()->createCommand()->flushIndex('customers');
$customers = Customer::find()->all(); $customers = Customer::find()->all();
$this->assertEquals(0, count($customers)); $this->assertEquals(0, count($customers));
$ret = Customer::deleteAll();
$this->assertEquals(0, $ret);
} }
} }
\ No newline at end of file
...@@ -161,6 +161,39 @@ class ActiveRecordTest extends RedisTestCase ...@@ -161,6 +161,39 @@ class ActiveRecordTest extends RedisTestCase
$this->assertTrue($customers['3-user3'] instanceof Customer); $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() public function testFindCount()
{ {
$this->assertEquals(3, Customer::find()->count()); $this->assertEquals(3, Customer::find()->count());
......
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