/** * @param \yii\db\ActiveRecord $model * @param string $relation * @param \yii\db\ActiveQuery $activeRelation * @return string * @throws Exception */ protected static function getRelationValue($model, $relation, $activeRelation) { $foreignKeys = array_values($activeRelation->link); $relation = Html::getAttributeName($relation); if ($activeRelation->multiple) { if (property_exists($model, $relation)) { // special case for search models, where there is a relation property defined that holds the keys $value = $model->{$relation}; } else { /** @var \yii\db\ActiveRecord $modelClass */ $modelClass = $activeRelation->modelClass; $value = array_map('\\netis\\crud\\crud\\Action::exportKey', $activeRelation->select($modelClass::primaryKey())->asArray()->all()); } if (is_array($value)) { $value = Action::implodeEscaped(Action::KEYS_SEPARATOR, $value); } return $value; } // special case for search models, where fks holds array of keys $foreignKey = reset($foreignKeys); if (!is_array($model->getAttribute($foreignKey))) { return Action::exportKey($model->getAttributes($foreignKeys)); } if (count($foreignKeys) > 1) { throw new Exception('Composite foreign keys are not supported for searching.'); } return Action::implodeEscaped(Action::KEYS_SEPARATOR, $model->getAttribute($foreignKey)); }
/** * @param \yii\db\ActiveQuery $query * @param string $attribute * @param int $from * @param int $size * @param int $offset * @param int|null $freeFrom * @param int $freeSize * @param int|null $targetDepth * @param array $additional * @return int|null * @throws Exception * @throws \yii\db\Exception */ protected function optimizeAttribute($query, $attribute, $from, $size, $offset = 0, $freeFrom = null, $freeSize = 0, $targetDepth = null, $additional = []) { $primaryKey = $this->getPrimaryKey(); $result = null; $isForward = $attribute === $this->leftAttribute; // @todo: pgsql and mssql optimization if (in_array($this->owner->getDb()->driverName, ['mysql', 'mysqli'])) { // mysql optimization $tableName = $this->owner->tableName(); $additionalString = null; $additionalParams = []; foreach ($additional as $name => $value) { $additionalString .= ", [[{$name}]] = "; if ($value instanceof Expression) { $additionalString .= $value->expression; foreach ($value->params as $n => $v) { $additionalParams[$n] = $v; } } else { $paramName = ':nestedIntervals' . count($additionalParams); $additionalString .= $paramName; $additionalParams[$paramName] = $value; } } $command = $query->select([$primaryKey, $attribute, $this->depthAttribute])->orderBy([$attribute => $isForward ? SORT_ASC : SORT_DESC])->createCommand(); $this->owner->getDb()->createCommand("\n UPDATE\n {$tableName} u,\n (SELECT\n [[{$primaryKey}]],\n IF (@i := @i + 1, 0, 0)\n + IF ([[{$attribute}]] " . ($isForward ? '>' : '<') . " @freeFrom,\n IF (\n (@result := @i)\n + IF (@depth - :targetDepth > 0, @result := @result + @depth - :targetDepth, 0)\n + (@i := @i + :freeSize * 2)\n + (@freeFrom := NULL), 0, 0),\n 0)\n + IF (@depth - [[{$this->depthAttribute}]] >= 0,\n IF (@i := @i + @depth - [[{$this->depthAttribute}]] + 1, 0, 0),\n 0)\n + (:from " . ($isForward ? '+' : '-') . " (CAST(@i AS UNSIGNED INTEGER) + :offset) * :size)\n + IF ([[{$attribute}]] = @freeFrom,\n IF ((@result := @i) + (@i := @i + :freeSize * 2) + (@freeFrom := NULL), 0, 0),\n 0)\n + IF (@depth := [[{$this->depthAttribute}]], 0, 0)\n as 'new'\n FROM\n (SELECT @i := 0, @depth := -1, @freeFrom := :freeFrom, @result := NULL) v,\n (" . $command->sql . ") t\n ) tmp\n SET u.[[{$attribute}]]=tmp.[[new]] {$additionalString}\n WHERE tmp.[[{$primaryKey}]]=u.[[{$primaryKey}]]")->bindValues($additionalParams)->bindValues($command->params)->bindValues([':from' => $from, ':size' => $size, ':offset' => $offset, ':freeFrom' => $freeFrom, ':freeSize' => $freeSize, ':targetDepth' => $targetDepth])->execute(); if ($freeFrom !== null) { $result = $this->owner->getDb()->createCommand("SELECT IFNULL(@result, @i + 1 + IF (@depth - :targetDepth > 0, @depth - :targetDepth, 0))")->bindValue(':targetDepth', $targetDepth)->queryScalar(); $result = $result === null ? null : (int) $result; } return $result; } else { // generic algorithm (very slow!) $query->select([$primaryKey, $attribute, $this->depthAttribute])->asArray()->orderBy([$attribute => $isForward ? SORT_ASC : SORT_DESC]); $prevDepth = -1; $i = 0; foreach ($query->each() as $data) { $i++; if ($freeFrom !== null && $freeFrom !== (int) $data[$attribute] && ($freeFrom > (int) $data[$attribute] xor $isForward)) { $result = $i; $depthDiff = $prevDepth - $targetDepth; if ($depthDiff > 0) { $result += $depthDiff; } $i += $freeSize * 2; $freeFrom = null; } $depthDiff = $prevDepth - $data[$this->depthAttribute]; if ($depthDiff >= 0) { $i += $depthDiff + 1; } $this->owner->updateAll(array_merge($additional, [$attribute => $isForward ? $from + ($i + $offset) * $size : $from - ($i + $offset) * $size]), [$primaryKey => $data[$primaryKey]]); if ($freeFrom !== null && $freeFrom === (int) $data[$attribute]) { $result = $i; $i += $freeSize * 2; $freeFrom = null; } $prevDepth = $data[$this->depthAttribute]; } if ($freeFrom !== null) { $result = $i + 1; $depthDiff = $prevDepth - $targetDepth; if ($depthDiff > 0) { $result += $depthDiff; } } return $result; } }
/** * Reestablishes links between current model and records from $relation specified by $keys. * Removes and inserts rows into a junction table. * @param \yii\db\ActiveQuery $relation * @param array $keys * @param array $removeKeys * @throws InvalidCallException */ private function linkJunctionByKeys($relation, $keys, $removeKeys = null) { /** @var \yii\db\ActiveRecord $owner */ $owner = $this->owner; if ($owner->getIsNewRecord()) { throw new InvalidCallException('Unable to link model: the model cannot be newly created.'); } /* @var $viaRelation \yii\db\ActiveQuery */ $viaRelation = is_array($relation->via) ? $relation->via[1] : $relation->via; if (is_array($relation->via)) { /* @var $viaClass \yii\db\ActiveRecord */ $viaClass = $viaRelation->modelClass; $viaTable = $viaClass::getTableSchema()->fullName; } else { /* @var $viaTable string */ $viaTable = reset($relation->via->from); } $schema = $owner::getDb()->getSchema(); $this->checkAccess($relation->modelClass, array_merge($keys, $removeKeys === null ? [] : $removeKeys), 'read'); if (!empty($keys) || !empty($removeKeys)) { $owner::getDb()->createCommand()->delete($viaTable, ['and', $removeKeys === null ? $this->buildKeyInCondition('not in', array_values($relation->link), $keys) : $this->buildKeyInCondition('in', array_values($relation->link), $removeKeys), array_combine(array_keys($viaRelation->link), $owner->getAttributes(array_values($viaRelation->link)))])->execute(); } if (empty($keys)) { return; } /** @var \yii\db\ActiveRecord $relationClass */ $relationClass = $relation->modelClass; $quotedViaTable = $schema->quoteTableName($viaTable); $quotedColumns = implode(', ', array_map([$schema, 'quoteColumnName'], array_merge(array_keys($viaRelation->link), array_values($relation->link)))); $prefixedPrimaryKeys = array_map(function ($c) { return 't.' . $c; }, array_keys($relation->link)); $prefixedForeignKeys = array_map(function ($c) { return 'j.' . $c; }, array_values($relation->link)); $alreadyLinked = $relation->select(array_keys($relation->link))->asArray()->all(); // a subquery is used as a more SQL portable way to specify list of values by putting them in a condition $subquery = (new Query())->select(array_merge(array_map(function ($c) use($schema) { return '(' . $schema->quoteValue($c) . ')'; }, $owner->getAttributes(array_values($viaRelation->link))), $prefixedPrimaryKeys))->from($relationClass::tableName() . ' t')->where($this->buildKeyInCondition('in', $prefixedPrimaryKeys, $keys))->andWhere($this->buildKeyInCondition('not in', $prefixedPrimaryKeys, $alreadyLinked)); if ($removeKeys === null) { $subquery->leftJoin($viaTable . ' j', array_merge(array_combine(array_map(function ($c) { return 'j.' . $c; }, array_keys($viaRelation->link)), $owner->getAttributes(array_values($viaRelation->link))), array_combine($prefixedForeignKeys, array_map(function ($k) { return new Expression($k); }, $prefixedPrimaryKeys)))); $subquery->andWhere(array_fill_keys($prefixedForeignKeys, null)); } list($subquery, $params) = $owner::getDb()->getQueryBuilder()->build($subquery); $query = "INSERT INTO {$quotedViaTable} ({$quotedColumns}) {$subquery}"; $owner::getDb()->createCommand($query, $params)->execute(); }
/** * Adds a condition to search in relations using subquery. * @todo this should be called for each token, to group their conditions with OR and group token groups with AND * * @param \yii\db\ActiveQuery $query * @param array $tokens all search tokens extracted from term * @param array $relationAttributes array of string(relation name) => array( * 'model' => netis\crud\db\ActiveRecord, * 'searchModel' => netis\crud\db\ActiveSearchTrait, * 'attributes' => array * ) * @return array conditions to add to $query */ protected function processSearchRelated(\yii\db\ActiveQuery $query, array $tokens, array $relationAttributes) { $allConditions = ['or']; foreach ($relationAttributes as $relationName => $relation) { /** * @todo optimize this (check first, don't want to loose another battle with PostgreSQL query planner): * - for BELONGS_TO check fk against subquery * - for HAS_MANY and HAS_ONE check pk against subquery * - for MANY_MANY join only to pivot table and check its fk agains subquery */ $query->joinWith([$relationName => function ($query) use($relationName) { /** @var \yii\db\ActiveQuery $query */ /** @var \yii\db\ActiveRecord $class */ $class = $query->modelClass; return $query->select(false)->from([$relationName => $class::tableName()]); }]); $conditions = ['and']; /** @var ActiveSearchInterface $searchModel */ $searchModel = $relation['searchModel']; if (!$searchModel instanceof ActiveSearchInterface) { continue; } foreach ($tokens as $token) { $condition = $searchModel->processSearchToken($token, $relation['attributes'], $relationName); if ($condition !== null) { $conditions[] = $condition; } } if ($conditions !== ['and']) { $allConditions[] = $conditions; } } return $allConditions !== ['or'] ? $allConditions : null; }