Browse Source

Merge pull request #1765 from yiisoft/elasticsearch-pk-refactoring

[WIP] Changed elasticsearch AR primary key handling
tags/2.0.0-beta
Carsten Brandt 11 years ago
parent
commit
f6530314e2
  1. 34
      extensions/yii/elasticsearch/ActiveQuery.php
  2. 135
      extensions/yii/elasticsearch/ActiveRecord.php
  3. 2
      extensions/yii/elasticsearch/CHANGELOG.md
  4. 12
      extensions/yii/elasticsearch/QueryBuilder.php
  5. 44
      extensions/yii/elasticsearch/README.md
  6. 2
      framework/yii/db/BaseActiveRecord.php
  7. 31
      tests/unit/data/ar/elasticsearch/Customer.php
  8. 27
      tests/unit/data/ar/elasticsearch/Item.php
  9. 38
      tests/unit/data/ar/elasticsearch/Order.php
  10. 25
      tests/unit/data/ar/elasticsearch/OrderItem.php
  11. 73
      tests/unit/extensions/elasticsearch/ActiveRecordTest.php
  12. 34
      tests/unit/framework/ar/ActiveRecordTestTrait.php

34
extensions/yii/elasticsearch/ActiveQuery.php

@ -90,16 +90,26 @@ class ActiveQuery extends Query implements ActiveQueryInterface
} }
unset($row); unset($row);
} }
/** @var ActiveRecord $modelClass */
$modelClass = $this->modelClass;
$pk = $modelClass::primaryKey()[0];
if ($this->asArray && $this->indexBy) { if ($this->asArray && $this->indexBy) {
foreach ($result['hits']['hits'] as &$row) { foreach ($result['hits']['hits'] as &$row) {
$row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id']; if ($pk === '_id') {
$row['_source']['_id'] = $row['_id'];
}
$row['_source']['_score'] = $row['_score'];
$row = $row['_source']; $row = $row['_source'];
} }
unset($row);
} }
$models = $this->createModels($result['hits']['hits']); $models = $this->createModels($result['hits']['hits']);
if ($this->asArray && !$this->indexBy) { if ($this->asArray && !$this->indexBy) {
foreach($models as $key => $model) { foreach($models as $key => $model) {
$model['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id']; if ($pk === '_id') {
$model['_source']['_id'] = $model['_id'];
}
$model['_source']['_score'] = $model['_score'];
$models[$key] = $model['_source']; $models[$key] = $model['_source'];
} }
} }
@ -123,8 +133,14 @@ class ActiveQuery extends Query implements ActiveQueryInterface
return null; return null;
} }
if ($this->asArray) { if ($this->asArray) {
/** @var ActiveRecord $modelClass */
$modelClass = $this->modelClass;
$model = $result['_source']; $model = $result['_source'];
$model[ActiveRecord::PRIMARY_KEY_NAME] = $result['_id']; $pk = $modelClass::primaryKey()[0];
if ($pk === '_id') {
$model['_id'] = $result['_id'];
}
$model['_score'] = $result['_score'];
} else { } else {
/** @var ActiveRecord $class */ /** @var ActiveRecord $class */
$class = $this->modelClass; $class = $this->modelClass;
@ -147,8 +163,14 @@ class ActiveQuery extends Query implements ActiveQueryInterface
if (!empty($result['hits']['hits'])) { if (!empty($result['hits']['hits'])) {
$models = $this->createModels($result['hits']['hits']); $models = $this->createModels($result['hits']['hits']);
if ($this->asArray) { if ($this->asArray) {
/** @var ActiveRecord $modelClass */
$modelClass = $this->modelClass;
$pk = $modelClass::primaryKey()[0];
foreach($models as $key => $model) { foreach($models as $key => $model) {
$model['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id']; if ($pk === '_id') {
$model['_source']['_id'] = $model['_id'];
}
$model['_source']['_score'] = $model['_score'];
$models[$key] = $model['_source']; $models[$key] = $model['_source'];
} }
} }
@ -167,7 +189,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
{ {
$record = parent::one($db); $record = parent::one($db);
if ($record !== false) { if ($record !== false) {
if ($field == ActiveRecord::PRIMARY_KEY_NAME) { if ($field == '_id') {
return $record['_id']; return $record['_id'];
} elseif (isset($record['_source'][$field])) { } elseif (isset($record['_source'][$field])) {
return $record['_source'][$field]; return $record['_source'][$field];
@ -181,7 +203,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
*/ */
public function column($field, $db = null) public function column($field, $db = null)
{ {
if ($field == ActiveRecord::PRIMARY_KEY_NAME) { if ($field == '_id') {
$command = $this->createCommand($db); $command = $this->createCommand($db);
$command->queryParts['fields'] = []; $command->queryParts['fields'] = [];
$result = $command->search(); $result = $command->search();

135
extensions/yii/elasticsearch/ActiveRecord.php

@ -44,9 +44,8 @@ use yii\helpers\StringHelper;
*/ */
class ActiveRecord extends BaseActiveRecord class ActiveRecord extends BaseActiveRecord
{ {
const PRIMARY_KEY_NAME = 'id';
private $_id; private $_id;
private $_score;
private $_version; private $_version;
/** /**
@ -67,14 +66,6 @@ class ActiveRecord extends BaseActiveRecord
{ {
$query = static::createQuery(); $query = static::createQuery();
if (is_array($q)) { if (is_array($q)) {
if (count($q) == 1 && (array_key_exists(ActiveRecord::PRIMARY_KEY_NAME, $q)) && $query->where === null) {
$pk = $q[ActiveRecord::PRIMARY_KEY_NAME];
if (is_array($pk)) {
return static::mget($pk);
} else {
return static::get($pk);
}
}
return $query->andWhere($q)->one(); return $query->andWhere($q)->one();
} elseif ($q !== null) { } elseif ($q !== null) {
return static::get($q); return static::get($q);
@ -155,9 +146,12 @@ class ActiveRecord extends BaseActiveRecord
// TODO implement copy and move as pk change is not possible // TODO implement copy and move as pk change is not possible
public function getId() /**
* @return float returns the score of this record when it was retrieved via a [[find()]] query.
*/
public function getScore()
{ {
return $this->_id; return $this->_score;
} }
/** /**
@ -165,10 +159,11 @@ class ActiveRecord extends BaseActiveRecord
* @param mixed $value * @param mixed $value
* @throws \yii\base\InvalidCallException when record is not new * @throws \yii\base\InvalidCallException when record is not new
*/ */
public function setId($value) public function setPrimaryKey($value)
{ {
if ($this->isNewRecord) { $pk = static::primaryKey()[0];
$this->_id = $value; if ($this->getIsNewRecord() || $pk != '_id') {
$this->$pk = $value;
} else { } else {
throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.');
} }
@ -179,10 +174,11 @@ class ActiveRecord extends BaseActiveRecord
*/ */
public function getPrimaryKey($asArray = false) public function getPrimaryKey($asArray = false)
{ {
$pk = static::primaryKey()[0];
if ($asArray) { if ($asArray) {
return [ActiveRecord::PRIMARY_KEY_NAME => $this->_id]; return [$pk => $this->$pk];
} else { } else {
return $this->_id; return $this->$pk;
} }
} }
@ -191,30 +187,53 @@ class ActiveRecord extends BaseActiveRecord
*/ */
public function getOldPrimaryKey($asArray = false) public function getOldPrimaryKey($asArray = false)
{ {
$id = $this->isNewRecord ? null : $this->_id; $pk = static::primaryKey()[0];
if ($this->getIsNewRecord()) {
$id = null;
} elseif ($pk == '_id') {
$id = $this->_id;
} else {
$id = $this->getOldAttribute($pk);
}
if ($asArray) { if ($asArray) {
return [ActiveRecord::PRIMARY_KEY_NAME => $id]; return [$pk => $id];
} else { } else {
return $this->_id; return $id;
} }
} }
/** /**
* This method defines the primary. * This method defines the attribute that uniquely identifies a record.
*
* The primaryKey for elasticsearch documents is the `_id` field by default. This field is not part of the
* ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]].
*
* You may overide this method to define the primary key name when you have defined
* [path mapping](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html)
* for the `_id` field so that it is part of the `_source` and thus part of the [[attributes()|attributes]].
* *
* The primaryKey for elasticsearch documents is always `primaryKey`. It can not be changed. * Note that elasticsearch only supports _one_ attribute to be the primary key. However to match the signature
* of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
* single string.
* *
* @return string[] the primary keys of this record. * @return string[] array of primary key attributes. Only the first element of the array will be used.
*/ */
public static function primaryKey() public static function primaryKey()
{ {
return [ActiveRecord::PRIMARY_KEY_NAME]; return ['_id'];
} }
/** /**
* Returns the list of all attribute names of the model. * Returns the list of all attribute names of the model.
*
* This method must be overridden by child classes to define available attributes. * This method must be overridden by child classes to define available attributes.
* @return array list of attribute names. *
* Attributes are names of fields of the corresponding elasticsearch document.
* The primaryKey for elasticsearch documents is the `_id` field by default which is not part of the attributes.
* You may define [path mapping](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html)
* for the `_id` field so that it is part of the `_source` fields and thus becomes part of the attributes.
*
* @return string[] list of attribute names.
*/ */
public function attributes() public function attributes()
{ {
@ -246,8 +265,11 @@ class ActiveRecord extends BaseActiveRecord
*/ */
public static function create($row) public static function create($row)
{ {
$row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id'];
$record = parent::create($row['_source']); $record = parent::create($row['_source']);
$pk = static::primaryKey()[0];
$record->$pk = $row['_id'];
$record->_score = isset($row['_score']) ? $row['_score'] : null;
$record->_version = isset($row['_version']) ? $row['_version'] : null; // TODO version should always be available...
return $record; return $record;
} }
@ -317,11 +339,16 @@ class ActiveRecord extends BaseActiveRecord
$options $options
); );
if (!$response['ok']) { if (!isset($response['ok'])) {
return false; return false;
} }
$this->_id = $response['_id']; $pk = static::primaryKey()[0];
$this->$pk = $response['_id'];
if ($pk != '_id') {
$values[$pk] = $response['_id'];
}
$this->_version = $response['_version']; $this->_version = $response['_version'];
$this->_score = null;
$this->setOldAttributes($values); $this->setOldAttributes($values);
$this->afterSave(true); $this->afterSave(true);
return true; return true;
@ -344,16 +371,17 @@ class ActiveRecord extends BaseActiveRecord
*/ */
public static function updateAll($attributes, $condition = []) public static function updateAll($attributes, $condition = [])
{ {
if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { $pkName = static::primaryKey()[0];
$primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; if (count($condition) == 1 && isset($condition[$pkName])) {
$primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]];
} else { } else {
$primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id
} }
if (empty($primaryKeys)) { if (empty($primaryKeys)) {
return 0; return 0;
} }
$bulk = ''; $bulk = '';
foreach((array) $primaryKeys as $pk) { foreach($primaryKeys as $pk) {
$action = Json::encode([ $action = Json::encode([
"update" => [ "update" => [
"_id" => $pk, "_id" => $pk,
@ -371,11 +399,17 @@ class ActiveRecord extends BaseActiveRecord
$url = [static::index(), static::type(), '_bulk']; $url = [static::index(), static::type(), '_bulk'];
$response = static::getDb()->post($url, [], $bulk); $response = static::getDb()->post($url, [], $bulk);
$n=0; $n=0;
$errors = [];
foreach($response['items'] as $item) { foreach($response['items'] as $item) {
if ($item['update']['ok']) { if (isset($item['update']['error'])) {
$errors[] = $item['update'];
} elseif ($item['update']['ok']) {
$n++; $n++;
} }
} }
if (!empty($errors)) {
throw new Exception(__METHOD__ . ' failed updating records.', $errors);
}
return $n; return $n;
} }
@ -395,16 +429,17 @@ class ActiveRecord extends BaseActiveRecord
*/ */
public static function updateAllCounters($counters, $condition = []) public static function updateAllCounters($counters, $condition = [])
{ {
if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { $pkName = static::primaryKey()[0];
$primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; if (count($condition) == 1 && isset($condition[$pkName])) {
$primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]];
} else { } else {
$primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id
} }
if (empty($primaryKeys) || empty($counters)) { if (empty($primaryKeys) || empty($counters)) {
return 0; return 0;
} }
$bulk = ''; $bulk = '';
foreach((array) $primaryKeys as $pk) { foreach($primaryKeys as $pk) {
$action = Json::encode([ $action = Json::encode([
"update" => [ "update" => [
"_id" => $pk, "_id" => $pk,
@ -426,13 +461,18 @@ class ActiveRecord extends BaseActiveRecord
// TODO do this via command // TODO do this via command
$url = [static::index(), static::type(), '_bulk']; $url = [static::index(), static::type(), '_bulk'];
$response = static::getDb()->post($url, [], $bulk); $response = static::getDb()->post($url, [], $bulk);
$n=0; $n=0;
$errors = [];
foreach($response['items'] as $item) { foreach($response['items'] as $item) {
if ($item['update']['ok']) { if (isset($item['update']['error'])) {
$errors[] = $item['update'];
} elseif ($item['update']['ok']) {
$n++; $n++;
} }
} }
if (!empty($errors)) {
throw new Exception(__METHOD__ . ' failed updating records counters.', $errors);
}
return $n; return $n;
} }
@ -452,16 +492,17 @@ class ActiveRecord extends BaseActiveRecord
*/ */
public static function deleteAll($condition = []) public static function deleteAll($condition = [])
{ {
if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { $pkName = static::primaryKey()[0];
$primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; if (count($condition) == 1 && isset($condition[$pkName])) {
$primaryKeys = is_array($condition[$pkName]) ? $condition[$pkName] : [$condition[$pkName]];
} else { } else {
$primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); $primaryKeys = static::find()->where($condition)->column($pkName); // TODO check whether this works with default pk _id
} }
if (empty($primaryKeys)) { if (empty($primaryKeys)) {
return 0; return 0;
} }
$bulk = ''; $bulk = '';
foreach((array) $primaryKeys as $pk) { foreach($primaryKeys as $pk) {
$bulk .= Json::encode([ $bulk .= Json::encode([
"delete" => [ "delete" => [
"_id" => $pk, "_id" => $pk,
@ -475,11 +516,17 @@ class ActiveRecord extends BaseActiveRecord
$url = [static::index(), static::type(), '_bulk']; $url = [static::index(), static::type(), '_bulk'];
$response = static::getDb()->post($url, [], $bulk); $response = static::getDb()->post($url, [], $bulk);
$n=0; $n=0;
$errors = [];
foreach($response['items'] as $item) { foreach($response['items'] as $item) {
if ($item['delete']['found'] && $item['delete']['ok']) { if (isset($item['delete']['error'])) {
$errors[] = $item['delete'];
} elseif ($item['delete']['found'] && $item['delete']['ok']) {
$n++; $n++;
} }
} }
if (!empty($errors)) {
throw new Exception(__METHOD__ . ' failed deleting records.', $errors);
}
return $n; return $n;
} }
} }

2
extensions/yii/elasticsearch/CHANGELOG.md

@ -5,6 +5,8 @@ Yii Framework 2 elasticsearch extension Change Log
---------------------------- ----------------------------
- Enh #1382: Added a debug toolbar panel for elasticsearch (cebe) - Enh #1382: Added a debug toolbar panel for elasticsearch (cebe)
- Enh #1765: Added support for primary key path mapping, pk can now be part of the attributes when mapping is defined (cebe)
- Chg #1765: Changed handling of ActiveRecord primary keys, removed getId(), use getPrimaryKey() instead (cebe)
2.0.0 alpha, December 1, 2013 2.0.0 alpha, December 1, 2013
----------------------------- -----------------------------

12
extensions/yii/elasticsearch/QueryBuilder.php

@ -114,7 +114,7 @@ class QueryBuilder extends \yii\base\Object
} else { } else {
$column = $name; $column = $name;
} }
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { if ($column == '_id') {
$column = '_uid'; $column = '_uid';
} }
@ -176,7 +176,7 @@ class QueryBuilder extends \yii\base\Object
{ {
$parts = []; $parts = [];
foreach($condition as $attribute => $value) { foreach($condition as $attribute => $value) {
if ($attribute == ActiveRecord::PRIMARY_KEY_NAME) { if ($attribute == '_id') {
if ($value == null) { // there is no null pk if ($value == null) { // there is no null pk
$parts[] = ['script' => ['script' => '0==1']]; $parts[] = ['script' => ['script' => '0==1']];
} else { } else {
@ -235,8 +235,8 @@ class QueryBuilder extends \yii\base\Object
} }
list($column, $value1, $value2) = $operands; list($column, $value1, $value2) = $operands;
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { if ($column == '_id') {
throw new NotSupportedException('Between condition is not supported for primaryKey.'); throw new NotSupportedException('Between condition is not supported for the _id field.');
} }
$filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]];
if ($operator == 'not between') { if ($operator == 'not between') {
@ -274,7 +274,7 @@ class QueryBuilder extends \yii\base\Object
unset($values[$i]); unset($values[$i]);
} }
} }
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { if ($column == '_id') {
if (empty($values) && $canBeNull) { // there is no null pk if (empty($values) && $canBeNull) { // there is no null pk
$filter = ['script' => ['script' => '0==1']]; $filter = ['script' => ['script' => '0==1']];
} else { } else {
@ -306,6 +306,6 @@ class QueryBuilder extends \yii\base\Object
private function buildLikeCondition($operator, $operands) private function buildLikeCondition($operator, $operands)
{ {
throw new NotSupportedException('like conditions is not supported by elasticsearch.'); throw new NotSupportedException('like conditions are not supported by elasticsearch.');
} }
} }

44
extensions/yii/elasticsearch/README.md

@ -55,12 +55,12 @@ For general information on how to use yii's ActiveRecord please refer to the [gu
For defining an elasticsearch ActiveRecord class your record class needs to extend from `yii\elasticsearch\ActiveRecord` and For defining an elasticsearch ActiveRecord class your record class needs to extend from `yii\elasticsearch\ActiveRecord` and
implement at least the `attributes()` method to define the attributes of the record. implement at least the `attributes()` method to define the attributes of the record.
The primary key (the `_id` field in elasticsearch terms) is represented by `getId()` and `setId()` and can not be changed. The handling of primary keys is different in elasticsearch as the primary key (the `_id` field in elasticsearch terms)
The primary key is not part of the attributes. is not part of the attributes by default. However it is possible to define a [path mapping](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html)
for the `_id` field to be part of the attributes.
primary key can be defined via [[primaryKey()]] which defaults to `id` if not specified. See [elasticsearch docs](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-id-field.html) on how to define it.
The primaryKey needs to be part of the attributes so make sure you have an `id` attribute defined if you do The `_id` field of a document/record can be accessed using [[ActiveRecord::getPrimaryKey()]] and [[ActiveRecord::setPrimaryKey()]].
not specify your own primary key. When path mapping is defined, the attribute name can be defined using the [[primaryKey()]] method.
The following is an example model called `Customer`: The following is an example model called `Customer`:
@ -72,6 +72,7 @@ class Customer extends \yii\elasticsearch\ActiveRecord
*/ */
public function attributes() public function attributes()
{ {
// path mapping for '_id' is setup to field 'id'
return ['id', 'name', 'address', 'registration_date']; return ['id', 'name', 'address', 'registration_date'];
} }
@ -105,25 +106,23 @@ It supports the same interface and features except the following limitations and
and [type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-type) to query against. and [type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-type) to query against.
- `select()` has been replaced with `fields()` which basically does the same but `fields` is more elasticsearch terminology. - `select()` has been replaced with `fields()` which basically does the same but `fields` is more elasticsearch terminology.
It defines the fields to retrieve from a document. It defines the fields to retrieve from a document.
- `via`-relations can not be defined via a table as there are not tables in elasticsearch. You can only define relations via other records. - `via`-relations can not be defined via a table as there are no tables in elasticsearch. You can only define relations via other records.
- As elasticsearch is a data storage and search engine there is of course support added for search your records. - As elasticsearch is not only a data storage but also a search engine there is of course support added for search your records.
There are `query()`, `filter()` and `addFacets()` methods that allows to compose an elasticsearch query. There are `query()`, `filter()` and `addFacets()` methods that allows to compose an elasticsearch query.
See the usage example below on how they work and check out the [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) See the usage example below on how they work and check out the [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html)
on how to compose `query` and `filter` parts. on how to compose `query` and `filter` parts.
- It is also possible to define relations from elasticsearch ActiveRecords to normal ActiveRecord classes and vice versa. - It is also possible to define relations from elasticsearch ActiveRecords to normal ActiveRecord classes and vice versa.
Elasticsearch separates primary key from attributes. You need to set the `id` property of the record to set its primary key.
Usage example: Usage example:
```php ```php
$customer = new Customer(); $customer = new Customer();
$customer->id = 1; $customer->primaryKey = 1; // in this case equivalent to $customer->id = 1;
$customer->attributes = ['name' => 'test']; $customer->attributes = ['name' => 'test'];
$customer->save(); $customer->save();
$customer = Customer::get(1); // get a record by pk $customer = Customer::get(1); // get a record by pk
$customers = Customer::get([1,2,3]); // get a records multiple by pk $customers = Customer::mget([1,2,3]); // get multiple records by pk
$customer = Customer::find()->where(['name' => 'test'])->one(); // find by query $customer = Customer::find()->where(['name' => 'test'])->one(); // find by query
$customers = Customer::find()->active()->all(); // find all by query (using the `active` scope) $customers = Customer::find()->active()->all(); // find all by query (using the `active` scope)
@ -152,10 +151,11 @@ Using the elasticsearch DebugPanel
---------------------------------- ----------------------------------
The yii2 elasticsearch extensions provides a `DebugPanel` that can be integrated with the yii debug module The yii2 elasticsearch extensions provides a `DebugPanel` that can be integrated with the yii debug module
an shows the executed elasticsearch queries. It also allows to run these queries on different cluster nodes and shows the executed elasticsearch queries. It also allows to run these queries
an view the results. and view the results.
Add the following to you application config to enable it: Add the following to you application config to enable it (if you already have the debug module
enabled, it is sufficient to just add the panels configuration):
```php ```php
// ... // ...
@ -174,3 +174,17 @@ Add the following to you application config to enable it:
``` ```
![elasticsearch DebugPanel](README-debug.png) ![elasticsearch DebugPanel](README-debug.png)
Relation definitions with records whose primary keys are not part of attributes
-------------------------------------------------------------------------------
TODO
Patterns
--------
### Fetching records from different indexes/types
TODO

2
framework/yii/db/BaseActiveRecord.php

@ -923,7 +923,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
*/ */
public function equals($record) public function equals($record)
{ {
if ($this->isNewRecord || $record->isNewRecord) { if ($this->getIsNewRecord() || $record->getIsNewRecord()) {
return false; return false;
} }
return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey(); return get_class($this) === get_class($record) && $this->getPrimaryKey() === $record->getPrimaryKey();

31
tests/unit/data/ar/elasticsearch/Customer.php

@ -1,6 +1,7 @@
<?php <?php
namespace yiiunit\data\ar\elasticsearch; namespace yiiunit\data\ar\elasticsearch;
use yii\elasticsearch\Command;
use yiiunit\extensions\elasticsearch\ActiveRecordTest; use yiiunit\extensions\elasticsearch\ActiveRecordTest;
/** /**
@ -19,14 +20,19 @@ class Customer extends ActiveRecord
public $status2; public $status2;
public static function primaryKey()
{
return ['id'];
}
public function attributes() public function attributes()
{ {
return ['name', 'email', 'address', 'status']; return ['id', 'name', 'email', 'address', 'status'];
} }
public function getOrders() public function getOrders()
{ {
return $this->hasMany(Order::className(), array('customer_id' => ActiveRecord::PRIMARY_KEY_NAME))->orderBy('create_time'); return $this->hasMany(Order::className(), array('customer_id' => 'id'))->orderBy('create_time');
} }
public static function active($query) public static function active($query)
@ -40,4 +46,25 @@ class Customer extends ActiveRecord
ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord;
parent::afterSave($insert); parent::afterSave($insert);
} }
/**
* sets up the index for this record
* @param Command $command
*/
public static function setUpMapping($command, $statusIsBoolean = false)
{
$command->deleteMapping(static::index(), static::type());
$command->setMapping(static::index(), static::type(), [
static::type() => [
"_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"],
"properties" => [
"name" => ["type" => "string", "index" => "not_analyzed"],
"email" => ["type" => "string", "index" => "not_analyzed"],
"address" => ["type" => "string", "index" => "analyzed"],
"status" => $statusIsBoolean ? ["type" => "boolean"] : ["type" => "integer"],
]
]
]);
}
} }

27
tests/unit/data/ar/elasticsearch/Item.php

@ -1,6 +1,7 @@
<?php <?php
namespace yiiunit\data\ar\elasticsearch; namespace yiiunit\data\ar\elasticsearch;
use yii\elasticsearch\Command;
/** /**
* Class Item * Class Item
@ -11,8 +12,32 @@ namespace yiiunit\data\ar\elasticsearch;
*/ */
class Item extends ActiveRecord class Item extends ActiveRecord
{ {
public static function primaryKey()
{
return ['id'];
}
public function attributes() public function attributes()
{ {
return ['name', 'category_id']; return ['id', 'name', 'category_id'];
}
/**
* sets up the index for this record
* @param Command $command
*/
public static function setUpMapping($command)
{
$command->deleteMapping(static::index(), static::type());
$command->setMapping(static::index(), static::type(), [
static::type() => [
"_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"],
"properties" => [
"name" => ["type" => "string", "index" => "not_analyzed"],
"category_id" => ["type" => "integer"],
]
]
]);
} }
} }

38
tests/unit/data/ar/elasticsearch/Order.php

@ -1,6 +1,7 @@
<?php <?php
namespace yiiunit\data\ar\elasticsearch; namespace yiiunit\data\ar\elasticsearch;
use yii\elasticsearch\Command;
/** /**
* Class Order * Class Order
@ -12,24 +13,29 @@ namespace yiiunit\data\ar\elasticsearch;
*/ */
class Order extends ActiveRecord class Order extends ActiveRecord
{ {
public static function primaryKey()
{
return ['id'];
}
public function attributes() public function attributes()
{ {
return ['customer_id', 'create_time', 'total']; return ['id', 'customer_id', 'create_time', 'total'];
} }
public function getCustomer() public function getCustomer()
{ {
return $this->hasOne(Customer::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'customer_id']); return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
} }
public function getOrderItems() public function getOrderItems()
{ {
return $this->hasMany(OrderItem::className(), ['order_id' => ActiveRecord::PRIMARY_KEY_NAME]); return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
} }
public function getItems() public function getItems()
{ {
return $this->hasMany(Item::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']) return $this->hasMany(Item::className(), ['id' => 'item_id'])
->via('orderItems')->orderBy('id'); ->via('orderItems')->orderBy('id');
} }
@ -51,8 +57,8 @@ class Order extends ActiveRecord
// public function getBooks() // public function getBooks()
// { // {
// return $this->hasMany('Item', [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']) // return $this->hasMany('Item', ['id' => 'item_id'])
// ->viaTable('tbl_order_item', ['order_id' => ActiveRecord::PRIMARY_KEY_NAME]) // ->viaTable('tbl_order_item', ['order_id' => 'id'])
// ->where(['category_id' => 1]); // ->where(['category_id' => 1]);
// } // }
@ -65,4 +71,24 @@ class Order extends ActiveRecord
return false; return false;
} }
} }
/**
* sets up the index for this record
* @param Command $command
*/
public static function setUpMapping($command)
{
$command->deleteMapping(static::index(), static::type());
$command->setMapping(static::index(), static::type(), [
static::type() => [
"_id" => ["path" => "id", "index" => "not_analyzed", "store" => "yes"],
"properties" => [
"customer_id" => ["type" => "integer"],
// "create_time" => ["type" => "string", "index" => "not_analyzed"],
"total" => ["type" => "integer"],
]
]
]);
}
} }

25
tests/unit/data/ar/elasticsearch/OrderItem.php

@ -1,6 +1,7 @@
<?php <?php
namespace yiiunit\data\ar\elasticsearch; namespace yiiunit\data\ar\elasticsearch;
use yii\elasticsearch\Command;
/** /**
* Class OrderItem * Class OrderItem
@ -19,11 +20,31 @@ class OrderItem extends ActiveRecord
public function getOrder() public function getOrder()
{ {
return $this->hasOne(Order::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'order_id']); return $this->hasOne(Order::className(), ['id' => 'order_id']);
} }
public function getItem() public function getItem()
{ {
return $this->hasOne(Item::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']); return $this->hasOne(Item::className(), ['id' => 'item_id']);
}
/**
* sets up the index for this record
* @param Command $command
*/
public static function setUpMapping($command)
{
$command->deleteMapping(static::index(), static::type());
$command->setMapping(static::index(), static::type(), [
static::type() => [
"properties" => [
"order_id" => ["type" => "integer"],
"item_id" => ["type" => "integer"],
"quantity" => ["type" => "integer"],
"subtotal" => ["type" => "integer"],
]
]
]);
} }
} }

73
tests/unit/extensions/elasticsearch/ActiveRecordTest.php

@ -47,18 +47,15 @@ class ActiveRecordTest extends ElasticSearchTestCase
if ($db->createCommand()->indexExists('yiitest')) { if ($db->createCommand()->indexExists('yiitest')) {
$db->createCommand()->deleteIndex('yiitest'); $db->createCommand()->deleteIndex('yiitest');
} }
$db->createCommand()->createIndex('yiitest');
$db->post(['yiitest'], [], Json::encode([ $command = $db->createCommand();
'mappings' => [ Customer::setUpMapping($command);
"item" => [ Item::setUpMapping($command);
"_source" => [ "enabled" => true ], Order::setUpMapping($command);
"properties" => [ OrderItem::setUpMapping($command);
// allow proper sorting by name
"name" => ["type" => "string", "index" => "not_analyzed"], $db->createCommand()->flushIndex('yiitest');
]
]
],
]));
$customer = new Customer(); $customer = new Customer();
$customer->id = 1; $customer->id = 1;
@ -132,6 +129,20 @@ class ActiveRecordTest extends ElasticSearchTestCase
$db->createCommand()->flushIndex('yiitest'); $db->createCommand()->flushIndex('yiitest');
} }
public function testFindAsArray()
{
// asArray
$customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one();
$this->assertEquals([
'id' => 2,
'email' => 'user2@example.com',
'name' => 'user2',
'address' => 'address2',
'status' => 1,
'_score' => 1.0
], $customer);
}
public function testSearch() public function testSearch()
{ {
$customers = $this->callCustomerFind()->search()['hits']; $customers = $this->callCustomerFind()->search()['hits'];
@ -243,8 +254,8 @@ class ActiveRecordTest extends ElasticSearchTestCase
public function testInsertNoPk() public function testInsertNoPk()
{ {
$this->assertEquals([ActiveRecord::PRIMARY_KEY_NAME], Customer::primaryKey()); $this->assertEquals(['id'], Customer::primaryKey());
$pkName = ActiveRecord::PRIMARY_KEY_NAME; $pkName = 'id';
$customer = new Customer; $customer = new Customer;
$customer->email = 'user4@example.com'; $customer->email = 'user4@example.com';
@ -257,6 +268,7 @@ class ActiveRecordTest extends ElasticSearchTestCase
$this->assertTrue($customer->isNewRecord); $this->assertTrue($customer->isNewRecord);
$customer->save(); $customer->save();
$this->afterSave();
$this->assertNotNull($customer->primaryKey); $this->assertNotNull($customer->primaryKey);
$this->assertNotNull($customer->oldPrimaryKey); $this->assertNotNull($customer->oldPrimaryKey);
@ -268,7 +280,7 @@ class ActiveRecordTest extends ElasticSearchTestCase
public function testInsertPk() public function testInsertPk()
{ {
$pkName = ActiveRecord::PRIMARY_KEY_NAME; $pkName = 'id';
$customer = new Customer; $customer = new Customer;
$customer->$pkName = 5; $customer->$pkName = 5;
@ -288,17 +300,26 @@ class ActiveRecordTest extends ElasticSearchTestCase
public function testUpdatePk() public function testUpdatePk()
{ {
$pkName = ActiveRecord::PRIMARY_KEY_NAME; $pkName = 'id';
$pk = [$pkName => 2]; $orderItem = Order::find([$pkName => 2]);
$orderItem = Order::find($pk);
$this->assertEquals(2, $orderItem->primaryKey); $this->assertEquals(2, $orderItem->primaryKey);
$this->assertEquals(2, $orderItem->oldPrimaryKey); $this->assertEquals(2, $orderItem->oldPrimaryKey);
$this->assertEquals(2, $orderItem->$pkName); $this->assertEquals(2, $orderItem->$pkName);
$this->setExpectedException('yii\base\InvalidCallException'); // $this->setExpectedException('yii\base\InvalidCallException');
$orderItem->$pkName = 13; $orderItem->$pkName = 13;
$this->assertEquals(13, $orderItem->primaryKey);
$this->assertEquals(2, $orderItem->oldPrimaryKey);
$this->assertEquals(13, $orderItem->$pkName);
$orderItem->save(); $orderItem->save();
$this->afterSave();
$this->assertEquals(13, $orderItem->primaryKey);
$this->assertEquals(13, $orderItem->oldPrimaryKey);
$this->assertEquals(13, $orderItem->$pkName);
$this->assertNull(Order::find([$pkName => 2]));
$this->assertNotNull(Order::find([$pkName => 13]));
} }
public function testFindLazyVia2() public function testFindLazyVia2()
@ -306,7 +327,7 @@ class ActiveRecordTest extends ElasticSearchTestCase
/** @var TestCase|ActiveRecordTestTrait $this */ /** @var TestCase|ActiveRecordTestTrait $this */
/** @var Order $order */ /** @var Order $order */
$orderClass = $this->getOrderClass(); $orderClass = $this->getOrderClass();
$pkName = ActiveRecord::PRIMARY_KEY_NAME; $pkName = 'id';
$order = new $orderClass(); $order = new $orderClass();
$order->$pkName = 100; $order->$pkName = 100;
@ -320,18 +341,8 @@ class ActiveRecordTest extends ElasticSearchTestCase
public function testBooleanAttribute() public function testBooleanAttribute()
{ {
$db = $this->getConnection(); $db = $this->getConnection();
$db->createCommand()->deleteIndex('yiitest'); Customer::setUpMapping($db->createCommand(), true);
$db->post(['yiitest'], [], Json::encode([ Customer::deleteAll();
'mappings' => [
"customer" => [
"_source" => [ "enabled" => true ],
"properties" => [
// this is for the boolean test
"status" => ["type" => "boolean"],
]
]
],
]));
$customerClass = $this->getCustomerClass(); $customerClass = $this->getCustomerClass();
$customer = new $customerClass(); $customer = new $customerClass();

34
tests/unit/framework/ar/ActiveRecordTestTrait.php

@ -145,7 +145,10 @@ trait ActiveRecordTestTrait
// scope // scope
$this->assertEquals(2, count($this->callCustomerFind()->active()->all())); $this->assertEquals(2, count($this->callCustomerFind()->active()->all()));
$this->assertEquals(2, $this->callCustomerFind()->active()->count()); $this->assertEquals(2, $this->callCustomerFind()->active()->count());
}
public function testFindAsArray()
{
// asArray // asArray
$customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one(); $customer = $this->callCustomerFind()->where(['id' => 2])->asArray()->one();
$this->assertEquals([ $this->assertEquals([
@ -494,6 +497,37 @@ trait ActiveRecordTestTrait
public function testFindEagerViaRelationPreserveOrder() public function testFindEagerViaRelationPreserveOrder()
{ {
/** @var TestCase|ActiveRecordTestTrait $this */ /** @var TestCase|ActiveRecordTestTrait $this */
/*
Item (name, category_id)
Order (customer_id, create_time, total)
OrderItem (order_id, item_id, quantity, subtotal)
Result should be the following:
Order 1: 1, 1325282384, 110.0
- orderItems:
OrderItem: 1, 1, 1, 30.0
OrderItem: 1, 2, 2, 40.0
- itemsInOrder:
Item 1: 'Agile Web Application Development with Yii1.1 and PHP5', 1
Item 2: 'Yii 1.1 Application Development Cookbook', 1
Order 2: 2, 1325334482, 33.0
- orderItems:
OrderItem: 2, 3, 1, 8.0
OrderItem: 2, 4, 1, 10.0
OrderItem: 2, 5, 1, 15.0
- itemsInOrder:
Item 5: 'Cars', 2
Item 3: 'Ice Age', 2
Item 4: 'Toy Story', 2
Order 3: 2, 1325502201, 40.0
- orderItems:
OrderItem: 3, 2, 1, 40.0
- itemsInOrder:
Item 3: 'Ice Age', 2
*/
$orders = $this->callOrderFind()->with('itemsInOrder1')->orderBy('create_time')->all(); $orders = $this->callOrderFind()->with('itemsInOrder1')->orderBy('create_time')->all();
$this->assertEquals(3, count($orders)); $this->assertEquals(3, count($orders));

Loading…
Cancel
Save