ActiveRecord.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\elasticsearch;
  8. use Yii;
  9. use yii\base\InvalidArgumentException;
  10. use yii\base\InvalidCallException;
  11. use yii\base\InvalidConfigException;
  12. use yii\base\NotSupportedException;
  13. use yii\db\ActiveQueryInterface;
  14. use yii\db\ActiveRecordInterface;
  15. use yii\db\BaseActiveRecord;
  16. use yii\db\StaleObjectException;
  17. use yii\helpers\ArrayHelper;
  18. use yii\helpers\Inflector;
  19. use yii\helpers\StringHelper;
  20. /**
  21. * ActiveRecord is the base class for classes representing relational data in terms of objects.
  22. *
  23. * This class implements the ActiveRecord pattern for the fulltext search and data storage
  24. * [Elasticsearch](https://www.elastic.co/products/elasticsearch).
  25. *
  26. * For defining a record a subclass should at least implement the [[attributes()]] method
  27. * to define attributes.
  28. * IMPORTANT: The primary key (the `_id` attribute) MUST NOT be included in [[attributes()]].
  29. *
  30. * The following is an example model called `Customer`:
  31. *
  32. * ```php
  33. * class Customer extends \yii\elasticsearch\ActiveRecord
  34. * {
  35. * public function attributes()
  36. * {
  37. * return ['name', 'address', 'registration_date'];
  38. * }
  39. * }
  40. * ```
  41. *
  42. * You may override [[index()]] and [[type()]] to define the index and type this record represents.
  43. * Types are being deprecated, and it is recommended to have a single type per index. For more information
  44. * read about [removal of mapping types](https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html).
  45. * For Elasticsearch 7 and later (as configured in [[Connection]], [[type()]] is ignored.
  46. *
  47. * @property mixed $_id The primary key of the record. Can only be written to for new records, otherwise read-only.
  48. * @property array|null $highlight A list of arrays with highlighted excerpts indexed by field names.
  49. * This property is read-only.
  50. * @property float $score Returns the score of this record when it was retrieved via a [[find()]] query.
  51. * This property is read-only.
  52. * @property array|null $explanation An explanation for each hit on how its score was computed.
  53. * This property is read-only.
  54. *
  55. * @author Carsten Brandt <mail@cebe.cc>
  56. * @since 2.0
  57. */
  58. class ActiveRecord extends BaseActiveRecord
  59. {
  60. private $_id;
  61. private $_score;
  62. private $_version;
  63. private $_highlight;
  64. private $_explanation;
  65. /**
  66. * Returns the database connection used by this AR class.
  67. * By default, the "elasticsearch" application component is used as the database connection.
  68. * You may override this method if you want to use a different database connection.
  69. * @return Connection the database connection used by this AR class.
  70. */
  71. public static function getDb()
  72. {
  73. return \Yii::$app->get('elasticsearch');
  74. }
  75. /**
  76. * @inheritdoc
  77. * @return ActiveQuery the newly created [[ActiveQuery]] instance.
  78. */
  79. public static function find()
  80. {
  81. return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
  82. }
  83. /**
  84. * @inheritdoc
  85. */
  86. public static function findOne($condition)
  87. {
  88. if (!is_array($condition)) {
  89. return static::get($condition);
  90. }
  91. if (!ArrayHelper::isAssociative($condition)) {
  92. $records = static::mget(array_values($condition));
  93. return empty($records) ? null : reset($records);
  94. }
  95. $condition = static::filterCondition($condition);
  96. return static::find()->andWhere($condition)->one();
  97. }
  98. /**
  99. * @inheritdoc
  100. */
  101. public static function findAll($condition)
  102. {
  103. if (!ArrayHelper::isAssociative($condition)) {
  104. return static::mget(is_array($condition) ? array_values($condition) : [$condition]);
  105. }
  106. $condition = static::filterCondition($condition);
  107. return static::find()->andWhere($condition)->all();
  108. }
  109. /**
  110. * Filter out condition parts that are array valued, to prevent building arbitrary conditions.
  111. * @param array $condition
  112. */
  113. private static function filterCondition($condition)
  114. {
  115. foreach ($condition as $k => $v) {
  116. if (is_array($v)) {
  117. $condition[$k] = array_values($v);
  118. foreach ($v as $vv) {
  119. if (is_array($vv)) {
  120. throw new InvalidArgumentException('Nested arrays are not allowed in condition for findAll() and findOne().');
  121. }
  122. }
  123. }
  124. }
  125. return $condition;
  126. }
  127. /**
  128. * Gets a record by its primary key.
  129. *
  130. * @param mixed $primaryKey the primaryKey value
  131. * @param array $options options given in this parameter are passed to Elasticsearch
  132. * as request URI parameters.
  133. * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html)
  134. * for more details on these options.
  135. * @return static|null The record instance or null if it was not found.
  136. */
  137. public static function get($primaryKey, $options = [])
  138. {
  139. if ($primaryKey === null) {
  140. return null;
  141. }
  142. $command = static::getDb()->createCommand();
  143. $result = $command->get(static::index(), static::type(), $primaryKey, $options);
  144. if ($result && $result['found']) {
  145. $model = static::instantiate($result);
  146. static::populateRecord($model, $result);
  147. $model->afterFind();
  148. return $model;
  149. }
  150. return null;
  151. }
  152. /**
  153. * Gets a list of records by its primary keys.
  154. *
  155. * @param array $primaryKeys an array of primaryKey values
  156. * @param array $options options given in this parameter are passed to Elasticsearch
  157. * as request URI parameters.
  158. *
  159. * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html)
  160. * for more details on these options.
  161. * @return array The record instances, or empty array if nothing was found
  162. */
  163. public static function mget(array $primaryKeys, $options = [])
  164. {
  165. if (empty($primaryKeys)) {
  166. return [];
  167. }
  168. if (count($primaryKeys) === 1) {
  169. $model = static::get(reset($primaryKeys));
  170. return $model === null ? [] : [$model];
  171. }
  172. $command = static::getDb()->createCommand();
  173. $result = $command->mget(static::index(), static::type(), $primaryKeys, $options);
  174. $models = [];
  175. foreach ($result['docs'] as $doc) {
  176. if ($doc['found']) {
  177. $model = static::instantiate($doc);
  178. static::populateRecord($model, $doc);
  179. $model->afterFind();
  180. $models[] = $model;
  181. }
  182. }
  183. return $models;
  184. }
  185. // TODO add more like this feature https://www.elastic.co/guide/en/elasticsearch/reference/current/search-more-like-this.html
  186. // TODO add percolate functionality https://www.elastic.co/guide/en/elasticsearch/reference/current/search-percolate.html
  187. // TODO implement copy and move as pk change is not possible
  188. /**
  189. * @return float returns the score of this record when it was retrieved via a [[find()]] query.
  190. */
  191. public function getScore()
  192. {
  193. return $this->_score;
  194. }
  195. /**
  196. * @return array|null A list of arrays with highlighted excerpts indexed by field names.
  197. */
  198. public function getHighlight()
  199. {
  200. return $this->_highlight;
  201. }
  202. /**
  203. * @return array|null An explanation for each hit on how its score was computed.
  204. * @since 2.0.5
  205. */
  206. public function getExplanation()
  207. {
  208. return $this->_explanation;
  209. }
  210. /**
  211. * Alias to [[get_id()]]. Returns the primary key value.
  212. * @param bool $asArray
  213. * @return mixed
  214. * @deprecated since 2.1.0
  215. */
  216. public function getPrimaryKey($asArray = false)
  217. {
  218. $pk = static::primaryKey()[0];
  219. if ($asArray) {
  220. return [$pk => $this->$pk];
  221. } else {
  222. return $this->$pk;
  223. }
  224. }
  225. /**
  226. * Alias to [[set_id()]]. Sets the primary key value.
  227. * @param mixed $value
  228. * @throws \yii\base\InvalidCallException when record is not new
  229. * @deprecated since 2.1.0
  230. */
  231. public function setPrimaryKey($value)
  232. {
  233. $pk = static::primaryKey()[0];
  234. $this->$pk = $value;
  235. }
  236. /**
  237. * Sets the `_id` attribute that holds the primary key (for compatibility with relations)
  238. * @param mixed $value
  239. * @throws \yii\base\InvalidCallException when record is not new
  240. */
  241. public function set_id($value)
  242. {
  243. $pk = static::primaryKey()[0];
  244. if ($this->getIsNewRecord()) {
  245. $this->$pk = $value;
  246. } else {
  247. throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.');
  248. }
  249. }
  250. /**
  251. * Returns the `_id` attribute that holds the primary key (for compatibility with relations)
  252. * @return mixed
  253. */
  254. public function get_id()
  255. {
  256. $pk = static::primaryKey()[0];
  257. return $this->$pk;
  258. }
  259. /**
  260. * @inheritdoc
  261. */
  262. public function getOldPrimaryKey($asArray = false)
  263. {
  264. $pk = static::primaryKey()[0];
  265. if ($this->getIsNewRecord()) {
  266. $id = null;
  267. } else {
  268. $id = $this->_id;
  269. }
  270. if ($asArray) {
  271. return [$pk => $id];
  272. } else {
  273. return $id;
  274. }
  275. }
  276. /**
  277. * This method defines the attribute that uniquely identifies a record.
  278. * The name of the primary key attribute is `_id`, and can not be changed.
  279. *
  280. * Elasticsearch does not support composite primary keys in the traditional sense. However to match the signature
  281. * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
  282. * single string.
  283. *
  284. * @return string[] array of primary key attributes. Only the first element of the array will be used.
  285. */
  286. final public static function primaryKey()
  287. {
  288. return ['_id'];
  289. }
  290. /**
  291. * Returns the list of all attribute names of the model.
  292. *
  293. * This method must be overridden by child classes to define available attributes.
  294. * IMPORTANT: The primary key (the `_id` attribute) MUST NOT be included in [[attributes()]].
  295. *
  296. * Attributes are names of fields of the corresponding Elasticsearch document.
  297. *
  298. * @return string[] list of attribute names.
  299. * @throws \yii\base\InvalidConfigException if not overridden in a child class.
  300. */
  301. public function attributes()
  302. {
  303. throw new InvalidConfigException('The attributes() method of Elasticsearch ActiveRecord has to be implemented by child classes.');
  304. }
  305. /**
  306. * A list of attributes that should be treated as array valued when retrieved through [[ActiveQuery::fields]].
  307. *
  308. * If not listed by this method, attributes retrieved through [[ActiveQuery::fields]] will converted to a scalar value
  309. * when the result array contains only one value.
  310. *
  311. * @return string[] list of attribute names. Must be a subset of [[attributes()]].
  312. */
  313. public function arrayAttributes()
  314. {
  315. return [];
  316. }
  317. /**
  318. * @return string the name of the index this record is stored in.
  319. */
  320. public static function index()
  321. {
  322. return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-'));
  323. }
  324. /**
  325. * Returns the name of the type of this record.
  326. * IMPORTANT: For Elasticsearch 7 and later, [[type()]] is ignored.
  327. * @return string the name of the type of this record.
  328. */
  329. public static function type()
  330. {
  331. return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
  332. }
  333. /**
  334. * @inheritdoc
  335. *
  336. * @param ActiveRecord $record the record to be populated. In most cases this will be an instance
  337. * created by [[instantiate()]] beforehand.
  338. * @param array $row attribute values (name => value)
  339. */
  340. public static function populateRecord($record, $row)
  341. {
  342. $attributes = [];
  343. if (isset($row['_source'])) {
  344. $attributes = $row['_source'];
  345. }
  346. if (isset($row['fields'])) {
  347. // reset fields in case it is scalar value
  348. $arrayAttributes = $record->arrayAttributes();
  349. foreach ($row['fields'] as $key => $value) {
  350. if (!isset($arrayAttributes[$key]) && count($value) === 1) {
  351. $row['fields'][$key] = reset($value);
  352. }
  353. }
  354. $attributes = array_merge($attributes, $row['fields']);
  355. }
  356. parent::populateRecord($record, $attributes);
  357. $pk = static::primaryKey()[0];
  358. $record->_id = $row[$pk];
  359. $record->_highlight = isset($row['highlight']) ? $row['highlight'] : null;
  360. $record->_score = isset($row['_score']) ? $row['_score'] : null;
  361. $record->_version = isset($row['_version']) ? $row['_version'] : null; // TODO version should always be available...
  362. $record->_explanation = isset($row['_explanation']) ? $row['_explanation'] : null;
  363. }
  364. /**
  365. * Creates an active record instance.
  366. *
  367. * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
  368. * It is not meant to be used for creating new records directly.
  369. *
  370. * You may override this method if the instance being created
  371. * depends on the row data to be populated into the record.
  372. * For example, by creating a record based on the value of a column,
  373. * you may implement the so-called single-table inheritance mapping.
  374. * @param array $row row data to be populated into the record.
  375. * This array consists of the following keys:
  376. * - `_source`: refers to the attributes of the record.
  377. * - `_type`: the type this record is stored in.
  378. * - `_index`: the index this record is stored in.
  379. * @return static the newly created active record
  380. */
  381. public static function instantiate($row)
  382. {
  383. return new static;
  384. }
  385. /**
  386. * Inserts a document into the associated index using the attribute values of this record.
  387. *
  388. * This method performs the following steps in order:
  389. *
  390. * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation
  391. * fails, it will skip the rest of the steps;
  392. * 2. call [[afterValidate()]] when `$runValidation` is true.
  393. * 3. call [[beforeSave()]]. If the method returns false, it will skip the
  394. * rest of the steps;
  395. * 4. insert the record into database. If this fails, it will skip the rest of the steps;
  396. * 5. call [[afterSave()]];
  397. *
  398. * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
  399. * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]]
  400. * will be raised by the corresponding methods.
  401. *
  402. * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
  403. *
  404. * If the [[primaryKey|primary key]] is not set (null) during insertion,
  405. * it will be populated with a randomly generated value after insertion.
  406. *
  407. * For example, to insert a customer record:
  408. *
  409. * ~~~
  410. * $customer = new Customer;
  411. * $customer->name = $name;
  412. * $customer->email = $email;
  413. * $customer->insert();
  414. * ~~~
  415. *
  416. * @param bool $runValidation whether to perform validation before saving the record.
  417. * If the validation fails, the record will not be inserted into the database.
  418. * @param array $attributes list of attributes that need to be saved. Defaults to null,
  419. * meaning all attributes will be saved.
  420. * @param array $options options given in this parameter are passed to Elasticsearch
  421. * as request URI parameters. These are among others:
  422. *
  423. * - `routing` define shard placement of this record.
  424. * - `parent` by giving the primaryKey of another record this defines a parent-child relation
  425. *
  426. * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html)
  427. * for more details on these options.
  428. *
  429. * By default the `op_type` is set to `create` if model primary key is present.
  430. * @return bool whether the attributes are valid and the record is inserted successfully.
  431. */
  432. public function insert($runValidation = true, $attributes = null, $options = [ ])
  433. {
  434. if ($runValidation && !$this->validate($attributes)) {
  435. return false;
  436. }
  437. if (!$this->beforeSave(true)) {
  438. return false;
  439. }
  440. $values = $this->getDirtyAttributes($attributes);
  441. if ($this->getPrimaryKey() !== null) {
  442. $options['op_type'] = isset($options['op_type']) ? $options['op_type'] : 'create';
  443. }
  444. $response = static::getDb()->createCommand()->insert(
  445. static::index(),
  446. static::type(),
  447. $values,
  448. $this->getPrimaryKey(),
  449. $options
  450. );
  451. if ($response === false) {
  452. return false;
  453. }
  454. $pk = static::primaryKey()[0];
  455. $this->$pk = $response['_id'];
  456. if ($pk != '_id') {
  457. $values[$pk] = $response['_id'];
  458. }
  459. $this->_version = $response['_version'];
  460. $this->_score = null;
  461. $changedAttributes = array_fill_keys(array_keys($values), null);
  462. $this->setOldAttributes($values);
  463. $this->afterSave(true, $changedAttributes);
  464. return true;
  465. }
  466. /**
  467. * @inheritdoc
  468. *
  469. * @param bool $runValidation whether to perform validation before saving the record.
  470. * If the validation fails, the record will not be inserted into the database.
  471. * @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
  472. * meaning all attributes that are loaded from DB will be saved.
  473. * @param array $options options given in this parameter are passed to Elasticsearch
  474. * as request URI parameters. These are among others:
  475. *
  476. * - `routing` define shard placement of this record.
  477. * - `parent` by giving the primaryKey of another record this defines a parent-child relation
  478. * - `timeout` timeout waiting for a shard to become available.
  479. * - `replication` the replication type for the delete/index operation (sync or async).
  480. * - `consistency` the write consistency of the index/delete operation.
  481. * - `refresh` refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately.
  482. * - `detect_noop` this parameter will become part of the request body and will prevent the index from getting updated when nothing has changed.
  483. *
  484. * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html#docs-update-api-query-params)
  485. * for more details on these options.
  486. *
  487. * The following parameters are Yii specific:
  488. *
  489. * - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it
  490. * has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]].
  491. * See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html) for details.
  492. *
  493. * Make sure the record has been fetched with a [[version]] before. This is only the case
  494. * for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly.
  495. *
  496. * @return int|bool the number of rows affected, or false if validation fails
  497. * or [[beforeSave()]] stops the updating process.
  498. * @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated.
  499. * @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled.
  500. * @throws Exception in case update failed.
  501. */
  502. public function update($runValidation = true, $attributeNames = null, $options = [])
  503. {
  504. if ($runValidation && !$this->validate($attributeNames)) {
  505. return false;
  506. }
  507. return $this->updateInternal($attributeNames, $options);
  508. }
  509. /**
  510. * @param array $attributes attributes to update
  511. * @param array $options options given in this parameter are passed to Elasticsearch
  512. * as request URI parameters. See [[update()]] for details.
  513. * @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
  514. * @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated.
  515. * @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled.
  516. * @throws Exception in case update failed.
  517. * @see update()
  518. */
  519. protected function updateInternal($attributes = null, $options = [])
  520. {
  521. if (!$this->beforeSave(false)) {
  522. return false;
  523. }
  524. $values = $this->getDirtyAttributes($attributes);
  525. if (empty($values)) {
  526. $this->afterSave(false, $values);
  527. return 0;
  528. }
  529. if (isset($options['optimistic_locking']) && $options['optimistic_locking']) {
  530. if ($this->_version === null) {
  531. throw new InvalidArgumentException('Unable to use optimistic locking on a record that has no version set. Refer to the docs of ActiveRecord::update() for details.');
  532. }
  533. $options['version'] = $this->_version;
  534. unset($options['optimistic_locking']);
  535. }
  536. try {
  537. $result = static::getDb()->createCommand()->update(
  538. static::index(),
  539. static::type(),
  540. $this->getOldPrimaryKey(false),
  541. $values,
  542. $options
  543. );
  544. } catch (Exception $e) {
  545. // HTTP 409 is the response in case of failed optimistic locking
  546. // https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html
  547. if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) {
  548. throw new StaleObjectException('The object being updated is outdated.', $e->errorInfo, $e->getCode(), $e);
  549. }
  550. throw $e;
  551. }
  552. if (is_array($result) && isset($result['_version'])) {
  553. $this->_version = $result['_version'];
  554. }
  555. $changedAttributes = [];
  556. foreach ($values as $name => $value) {
  557. $changedAttributes[$name] = $this->getOldAttribute($name);
  558. $this->setOldAttribute($name, $value);
  559. }
  560. $this->afterSave(false, $changedAttributes);
  561. if ($result === false) {
  562. return 0;
  563. } else {
  564. return 1;
  565. }
  566. }
  567. /**
  568. * Performs a quick and highly efficient scroll/scan query to get the list of primary keys that
  569. * satisfy the given condition. If condition is a list of primary keys
  570. * (e.g.: `['_id' => ['1', '2', '3']]`), the query is not performed for performance considerations.
  571. * @param array $condition please refer to [[ActiveQuery::where()]] on how to specify this parameter
  572. * @return array primary keys that correspond to given conditions
  573. * @see updateAll()
  574. * @see updateAllCounters()
  575. * @see deleteAll()
  576. * @since 2.0.4
  577. */
  578. protected static function primaryKeysByCondition($condition)
  579. {
  580. $pkName = static::primaryKey()[0];
  581. if (count($condition) == 1 && isset($condition[$pkName])) {
  582. $primaryKeys = (array)$condition[$pkName];
  583. } else {
  584. //fetch only document metadata (no fields), 1000 documents per shard
  585. $query = static::find()->where($condition)->asArray()->source(false)->limit(1000);
  586. $primaryKeys = [];
  587. foreach ($query->each('1m') as $document) {
  588. $primaryKeys[] = $document['_id'];
  589. }
  590. }
  591. return $primaryKeys;
  592. }
  593. /**
  594. * Updates all records that match a certain condition.
  595. * For example, to change the status to be 1 for all customers whose status is 2:
  596. *
  597. * ~~~
  598. * Customer::updateAll(['status' => 1], ['status' => 2]);
  599. * ~~~
  600. *
  601. * @param array $attributes attribute values (name-value pairs) to be saved into the table
  602. * @param array $condition the conditions that will be passed to the `where()` method when building the query.
  603. * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
  604. * @return int the number of rows updated
  605. * @throws Exception on error.
  606. * @see [[ActiveRecord::primaryKeysByCondition()]]
  607. */
  608. public static function updateAll($attributes, $condition = [])
  609. {
  610. $primaryKeys = static::primaryKeysByCondition($condition);
  611. if (empty($primaryKeys)) {
  612. return 0;
  613. }
  614. $bulkCommand = static::getDb()->createBulkCommand([
  615. "index" => static::index(),
  616. "type" => static::type(),
  617. ]);
  618. foreach ($primaryKeys as $pk) {
  619. $bulkCommand->addAction(["update" => ["_id" => $pk]], ["doc" => $attributes]);
  620. }
  621. $response = $bulkCommand->execute();
  622. $n = 0;
  623. $errors = [];
  624. foreach ($response['items'] as $item) {
  625. if (isset($item['update']['status']) && $item['update']['status'] == 200) {
  626. $n++;
  627. } else {
  628. $errors[] = $item['update'];
  629. }
  630. }
  631. if (!empty($errors) || isset($response['errors']) && $response['errors']) {
  632. throw new Exception(__METHOD__ . ' failed updating records.', $errors);
  633. }
  634. return $n;
  635. }
  636. /**
  637. * Updates all matching records using the provided counter changes and conditions.
  638. * For example, to add 1 to age of all customers whose status is 2,
  639. *
  640. * ~~~
  641. * Customer::updateAllCounters(['age' => 1], ['status' => 2]);
  642. * ~~~
  643. *
  644. * @param array $counters the counters to be updated (attribute name => increment value).
  645. * Use negative values if you want to decrement the counters.
  646. * @param array $condition the conditions that will be passed to the `where()` method when building the query.
  647. * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
  648. * @return int the number of rows updated
  649. * @throws Exception on error.
  650. * @see [[ActiveRecord::primaryKeysByCondition()]]
  651. */
  652. public static function updateAllCounters($counters, $condition = [])
  653. {
  654. $primaryKeys = static::primaryKeysByCondition($condition);
  655. if (empty($primaryKeys) || empty($counters)) {
  656. return 0;
  657. }
  658. $bulkCommand = static::getDb()->createBulkCommand([
  659. "index" => static::index(),
  660. "type" => static::type(),
  661. ]);
  662. foreach ($primaryKeys as $pk) {
  663. $script = '';
  664. foreach ($counters as $counter => $value) {
  665. $script .= "ctx._source.{$counter} += params.{$counter};\n";
  666. }
  667. $bulkCommand->addAction(["update" => ["_id" => $pk]], [
  668. 'script' => [
  669. 'inline' => $script,
  670. 'params' => $counters,
  671. 'lang' => 'painless',
  672. ],
  673. ]);
  674. }
  675. $response = $bulkCommand->execute();
  676. $n = 0;
  677. $errors = [];
  678. foreach ($response['items'] as $item) {
  679. if (isset($item['update']['status']) && $item['update']['status'] == 200) {
  680. $n++;
  681. } else {
  682. $errors[] = $item['update'];
  683. }
  684. }
  685. if (!empty($errors) || isset($response['errors']) && $response['errors']) {
  686. throw new Exception(__METHOD__ . ' failed updating records counters.', $errors);
  687. }
  688. return $n;
  689. }
  690. /**
  691. * @inheritdoc
  692. *
  693. * @param array $options options given in this parameter are passed to Elasticsearch
  694. * as request URI parameters. These are among others:
  695. *
  696. * - `routing` define shard placement of this record.
  697. * - `parent` by giving the primaryKey of another record this defines a parent-child relation
  698. * - `timeout` timeout waiting for a shard to become available.
  699. * - `replication` the replication type for the delete/index operation (sync or async).
  700. * - `consistency` the write consistency of the index/delete operation.
  701. * - `refresh` refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately.
  702. *
  703. * Please refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html)
  704. * for more details on these options.
  705. *
  706. * The following parameters are Yii specific:
  707. *
  708. * - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it
  709. * has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]].
  710. * See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html#delete-versioning) for details.
  711. *
  712. * Make sure the record has been fetched with a [[version]] before. This is only the case
  713. * for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly.
  714. *
  715. * @return int|bool the number of rows deleted, or false if the deletion is unsuccessful for some reason.
  716. * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
  717. * @throws StaleObjectException if optimistic locking is enabled and the data being deleted is outdated.
  718. * @throws Exception in case delete failed.
  719. */
  720. public function delete($options = [])
  721. {
  722. if (!$this->beforeDelete()) {
  723. return false;
  724. }
  725. if (isset($options['optimistic_locking']) && $options['optimistic_locking']) {
  726. if ($this->_version === null) {
  727. throw new InvalidArgumentException('Unable to use optimistic locking on a record that has no version set. Refer to the docs of ActiveRecord::delete() for details.');
  728. }
  729. $options['version'] = $this->_version;
  730. unset($options['optimistic_locking']);
  731. }
  732. try {
  733. $result = static::getDb()->createCommand()->delete(
  734. static::index(),
  735. static::type(),
  736. $this->getOldPrimaryKey(false),
  737. $options
  738. );
  739. } catch (Exception $e) {
  740. // HTTP 409 is the response in case of failed optimistic locking
  741. // https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html
  742. if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) {
  743. throw new StaleObjectException('The object being deleted is outdated.', $e->errorInfo, $e->getCode(), $e);
  744. }
  745. throw $e;
  746. }
  747. $this->setOldAttributes(null);
  748. $this->afterDelete();
  749. if ($result === false) {
  750. return 0;
  751. } else {
  752. return 1;
  753. }
  754. }
  755. /**
  756. * Deletes rows in the table using the provided conditions.
  757. * WARNING: If you do not specify any condition, this method will delete ALL rows in the table.
  758. *
  759. * For example, to delete all customers whose status is 3:
  760. *
  761. * ~~~
  762. * Customer::deleteAll(['status' => 3]);
  763. * ~~~
  764. *
  765. * @param array $condition the conditions that will be passed to the `where()` method when building the query.
  766. * Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
  767. * @return int the number of rows deleted
  768. * @throws Exception on error.
  769. * @see [[ActiveRecord::primaryKeysByCondition()]]
  770. */
  771. public static function deleteAll($condition = [])
  772. {
  773. $primaryKeys = static::primaryKeysByCondition($condition);
  774. if (empty($primaryKeys)) {
  775. return 0;
  776. }
  777. $bulkCommand = static::getDb()->createBulkCommand([
  778. "index" => static::index(),
  779. "type" => static::type(),
  780. ]);
  781. foreach ($primaryKeys as $pk) {
  782. $bulkCommand->addDeleteAction($pk);
  783. }
  784. $response = $bulkCommand->execute();
  785. $n = 0;
  786. $errors = [];
  787. foreach ($response['items'] as $item) {
  788. if (isset($item['delete']['status']) && $item['delete']['status'] == 200) {
  789. if (isset($item['delete']['found']) && $item['delete']['found']) {
  790. # ES5 uses "found"
  791. $n++;
  792. } elseif (isset($item['delete']['result']) && $item['delete']['result'] == "deleted") {
  793. # ES6 uses "result"
  794. $n++;
  795. }
  796. } else {
  797. $errors[] = $item['delete'];
  798. }
  799. }
  800. if (!empty($errors) || isset($response['errors']) && $response['errors']) {
  801. throw new Exception(__METHOD__ . ' failed deleting records.', $errors);
  802. }
  803. return $n;
  804. }
  805. /**
  806. * This method has no effect in Elasticsearch ActiveRecord.
  807. *
  808. * Elasticsearch ActiveRecord uses [native Optimistic locking](https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html).
  809. * See [[update()]] for more details.
  810. */
  811. public function optimisticLock()
  812. {
  813. return null;
  814. }
  815. /**
  816. * Destroys the relationship in current model.
  817. *
  818. * This method is not supported by Elasticsearch.
  819. */
  820. public function unlinkAll($name, $delete = false)
  821. {
  822. throw new NotSupportedException('unlinkAll() is not supported by Elasticsearch, use unlink() instead.');
  823. }
  824. public function link($name, $model, $extraColumns = [])
  825. {
  826. $relation = $this->getRelation($name);
  827. if ($relation->via === null) {
  828. $this->validateViaRelationLink($model, $relation);
  829. }
  830. parent::link($name, $model, $extraColumns);
  831. }
  832. /**
  833. * Validates model so that it does not contain array as its keys while linking.
  834. *
  835. * @param ActiveRecordInterface $model the model to be linked with the current one.
  836. * @param ActiveQueryInterface|ActiveQuery the relational query object.
  837. */
  838. protected function validateViaRelationLink($model, $relation)
  839. {
  840. $p1 = $model->isPrimaryKey(array_keys($relation->link));
  841. $p2 = static::isPrimaryKey(array_values($relation->link));
  842. $atLeastOneExists = !$this->getIsNewRecord() || !$model->getIsNewRecord();
  843. $foreign = null;
  844. $link = null;
  845. if ($p1 && $p2 && $atLeastOneExists) {
  846. if ($this->getIsNewRecord()) {
  847. $foreign = $this;
  848. $link = array_flip($relation->link);
  849. } else {
  850. $foreign = $model;
  851. $link = $relation->link;
  852. }
  853. } elseif ($p1) {
  854. $foreign = $this;
  855. $link = array_flip($relation->link);
  856. } elseif ($p2) {
  857. $foreign = $model;
  858. $link = $relation->link;
  859. }
  860. if ($foreign && $link) {
  861. foreach ($link as $fk => $pk) {
  862. if (is_array($foreign->{$fk})) {
  863. throw new InvalidCallException('Unable to link models: foreign model cannot be linked if its property is an array.');
  864. }
  865. }
  866. }
  867. }
  868. }