ActiveQuery.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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\db\ActiveQueryInterface;
  9. use yii\db\ActiveQueryTrait;
  10. use yii\db\ActiveRelationTrait;
  11. /**
  12. * ActiveQuery represents a [[Query]] associated with an [[ActiveRecord]] class.
  13. *
  14. * An ActiveQuery can be a normal query or be used in a relational context.
  15. *
  16. * ActiveQuery instances are usually created by [[ActiveRecord::find()]].
  17. * Relational queries are created by [[ActiveRecord::hasOne()]] and [[ActiveRecord::hasMany()]].
  18. *
  19. * Normal Query
  20. * ------------
  21. *
  22. * ActiveQuery mainly provides the following methods to retrieve the query results:
  23. *
  24. * - [[one()]]: returns a single record populated with the first row of data.
  25. * - [[all()]]: returns all records based on the query results.
  26. * - [[count()]]: returns the number of records.
  27. * - [[scalar()]]: returns the value of the first column in the first row of the query result.
  28. * - [[column()]]: returns the value of the first column in the query result.
  29. * - [[exists()]]: returns a value indicating whether the query result has data or not.
  30. *
  31. * Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]],
  32. * [[orderBy()]] to customize the query options.
  33. *
  34. * ActiveQuery also provides the following additional query options:
  35. *
  36. * - [[with()]]: list of relations that this query should be performed with.
  37. * - [[indexBy()]]: the name of the column by which the query result should be indexed.
  38. * - [[asArray()]]: whether to return each record as an array.
  39. *
  40. * These options can be configured using methods of the same name. For example:
  41. *
  42. * ```php
  43. * $customers = Customer::find()->with('orders')->asArray()->all();
  44. * ```
  45. * > NOTE: Elasticsearch limits the number of records returned to 10 records by default.
  46. * > If you expect to get more records you should specify limit explicitly.
  47. *
  48. * Relational query
  49. * ----------------
  50. *
  51. * In relational context ActiveQuery represents a relation between two Active Record classes.
  52. *
  53. * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
  54. * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
  55. * a getter method which calls one of the above methods and returns the created ActiveQuery object.
  56. *
  57. * A relation is specified by [[link]] which represents the association between columns
  58. * of different tables; and the multiplicity of the relation is indicated by [[multiple]].
  59. *
  60. * If a relation involves a junction table, it may be specified by [[via()]].
  61. * This methods may only be called in a relational context. Same is true for [[inverseOf()]], which
  62. * marks a relation as inverse of another relation.
  63. *
  64. * > Note: Elasticsearch limits the number of records returned by any query to 10 records by default.
  65. * > If you expect to get more records you should specify limit explicitly in relation definition.
  66. * > This is also important for relations that use [[via()]] so that if via records are limited to 10
  67. * > the relations records can also not be more than 10.
  68. *
  69. * > Note: Currently [[with]] is not supported in combination with [[asArray]].
  70. *
  71. * @author Carsten Brandt <mail@cebe.cc>
  72. * @since 2.0
  73. */
  74. class ActiveQuery extends Query implements ActiveQueryInterface
  75. {
  76. use ActiveQueryTrait;
  77. use ActiveRelationTrait;
  78. /**
  79. * @event Event an event that is triggered when the query is initialized via [[init()]].
  80. */
  81. const EVENT_INIT = 'init';
  82. /**
  83. * Constructor.
  84. * @param string $modelClass the model class associated with this query
  85. * @param array $config configurations to be applied to the newly created query object
  86. */
  87. public function __construct($modelClass, $config = [])
  88. {
  89. $this->modelClass = $modelClass;
  90. parent::__construct($config);
  91. }
  92. /**
  93. * Initializes the object.
  94. * This method is called at the end of the constructor. The default implementation will trigger
  95. * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
  96. * to ensure triggering of the event.
  97. */
  98. public function init()
  99. {
  100. parent::init();
  101. $this->trigger(self::EVENT_INIT);
  102. }
  103. /**
  104. * Creates a DB command that can be used to execute this query.
  105. * @param Connection $db the DB connection used to create the DB command.
  106. * If null, the DB connection returned by [[modelClass]] will be used.
  107. * @return Command the created DB command instance.
  108. */
  109. public function createCommand($db = null)
  110. {
  111. if ($this->primaryModel !== null) {
  112. // lazy loading
  113. if (is_array($this->via)) {
  114. // via relation
  115. /* @var $viaQuery ActiveQuery */
  116. list($viaName, $viaQuery) = $this->via;
  117. if ($viaQuery->multiple) {
  118. $viaModels = $viaQuery->all();
  119. $this->primaryModel->populateRelation($viaName, $viaModels);
  120. } else {
  121. $model = $viaQuery->one();
  122. $this->primaryModel->populateRelation($viaName, $model);
  123. $viaModels = $model === null ? [] : [$model];
  124. }
  125. $this->filterByModels($viaModels);
  126. } else {
  127. $this->filterByModels([$this->primaryModel]);
  128. }
  129. }
  130. /* @var $modelClass ActiveRecord */
  131. $modelClass = $this->modelClass;
  132. if ($db === null) {
  133. $db = $modelClass::getDb();
  134. }
  135. if ($this->type === null) {
  136. $this->type = $modelClass::type();
  137. }
  138. if ($this->index === null) {
  139. $this->index = $modelClass::index();
  140. $this->type = $modelClass::type();
  141. }
  142. $commandConfig = $db->getQueryBuilder()->build($this);
  143. return $db->createCommand($commandConfig);
  144. }
  145. /**
  146. * Executes query and returns all results as an array.
  147. * @param Connection $db the DB connection used to create the DB command.
  148. * If null, the DB connection returned by [[modelClass]] will be used.
  149. * @return array the query results. If the query results in nothing, an empty array will be returned.
  150. */
  151. public function all($db = null)
  152. {
  153. return parent::all($db);
  154. }
  155. /**
  156. * Converts found rows into model instances
  157. * @param array $rows
  158. * @return array|ActiveRecord[]
  159. * @since 2.0.4
  160. */
  161. private function createModels($rows)
  162. {
  163. $models = [];
  164. if ($this->asArray) {
  165. if ($this->indexBy === null) {
  166. return $rows;
  167. }
  168. foreach ($rows as $row) {
  169. if (is_string($this->indexBy)) {
  170. $key = isset($row['fields'][$this->indexBy]) ? reset($row['fields'][$this->indexBy]) : $row['_source'][$this->indexBy];
  171. } else {
  172. $key = call_user_func($this->indexBy, $row);
  173. }
  174. $models[$key] = $row;
  175. }
  176. } else {
  177. /* @var $class ActiveRecord */
  178. $class = $this->modelClass;
  179. if ($this->indexBy === null) {
  180. foreach ($rows as $row) {
  181. $model = $class::instantiate($row);
  182. $modelClass = get_class($model);
  183. $modelClass::populateRecord($model, $row);
  184. $models[] = $model;
  185. }
  186. } else {
  187. foreach ($rows as $row) {
  188. $model = $class::instantiate($row);
  189. $modelClass = get_class($model);
  190. $modelClass::populateRecord($model, $row);
  191. if (is_string($this->indexBy)) {
  192. $key = $model->{$this->indexBy};
  193. } else {
  194. $key = call_user_func($this->indexBy, $model);
  195. }
  196. $models[$key] = $model;
  197. }
  198. }
  199. }
  200. return $models;
  201. }
  202. /**
  203. * @inheritdoc
  204. * @since 2.0.4
  205. */
  206. public function populate($rows)
  207. {
  208. if (empty($rows)) {
  209. return [];
  210. }
  211. $models = $this->createModels($rows);
  212. if (!empty($this->with)) {
  213. $this->findWith($this->with, $models);
  214. }
  215. if (!$this->asArray) {
  216. foreach ($models as $model) {
  217. $model->afterFind();
  218. }
  219. }
  220. return $models;
  221. }
  222. /**
  223. * Executes query and returns a single row of result.
  224. * @param Connection $db the DB connection used to create the DB command.
  225. * If null, the DB connection returned by [[modelClass]] will be used.
  226. * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
  227. * the query result may be either an array or an ActiveRecord object. Null will be returned
  228. * if the query results in nothing.
  229. */
  230. public function one($db = null)
  231. {
  232. if (($result = parent::one($db)) === false) {
  233. return null;
  234. }
  235. if ($this->asArray) {
  236. // TODO implement with()
  237. // /* @var $modelClass ActiveRecord */
  238. // $modelClass = $this->modelClass;
  239. // $model = $result['_source'];
  240. // $pk = $modelClass::primaryKey()[0];
  241. // if ($pk === '_id') {
  242. // $model['_id'] = $result['_id'];
  243. // }
  244. // $model['_score'] = $result['_score'];
  245. // if (!empty($this->with)) {
  246. // $models = [$model];
  247. // $this->findWith($this->with, $models);
  248. // $model = $models[0];
  249. // }
  250. return $result;
  251. } else {
  252. /* @var $class ActiveRecord */
  253. $class = $this->modelClass;
  254. $model = $class::instantiate($result);
  255. $class = get_class($model);
  256. $class::populateRecord($model, $result);
  257. if (!empty($this->with)) {
  258. $models = [$model];
  259. $this->findWith($this->with, $models);
  260. $model = $models[0];
  261. }
  262. $model->afterFind();
  263. return $model;
  264. }
  265. }
  266. /**
  267. * @inheritdoc
  268. */
  269. public function search($db = null, $options = [])
  270. {
  271. if ($this->emulateExecution) {
  272. return [
  273. 'hits' => [
  274. 'total' => 0,
  275. 'hits' => [],
  276. ],
  277. ];
  278. }
  279. $command = $this->createCommand($db);
  280. $result = $command->search($options);
  281. if ($result === false) {
  282. throw new Exception('Elasticsearch search query failed.', [
  283. 'index' => $command->index,
  284. 'type' => $command->type,
  285. 'query' => $command->queryParts,
  286. 'options' => $command->options,
  287. ]);
  288. }
  289. // TODO implement with() for asArray
  290. if (!empty($result['hits']['hits']) && !$this->asArray) {
  291. $models = $this->createModels($result['hits']['hits']);
  292. if (!empty($this->with)) {
  293. $this->findWith($this->with, $models);
  294. }
  295. foreach ($models as $model) {
  296. $model->afterFind();
  297. }
  298. $result['hits']['hits'] = $models;
  299. }
  300. return $result;
  301. }
  302. /**
  303. * @inheritdoc
  304. */
  305. public function column($field, $db = null)
  306. {
  307. if ($field === '_id') {
  308. $command = $this->createCommand($db);
  309. $command->queryParts['_source'] = false;
  310. $result = $command->search();
  311. if ($result === false) {
  312. throw new Exception('Elasticsearch search query failed.');
  313. }
  314. if (empty($result['hits']['hits'])) {
  315. return [];
  316. }
  317. $column = [];
  318. foreach ($result['hits']['hits'] as $row) {
  319. $column[] = $row['_id'];
  320. }
  321. return $column;
  322. }
  323. return parent::column($field, $db);
  324. }
  325. }