Проблема default scope в Yii2 существует уже больше года, еще с того времени, как Yii2 был в бета версии. Соответственно при поиске нас кидает на различные обсуждения, среди которых преобладает не гибкое решение, препятствующее нормальному наследованию классов.
Я хочу поделиться своим вариантом, который решает проблемы наследования.
Не гибкое решение Default scope, которое преобладает при поиске, выглядит так:
class ActiveRecord extends \yii\db\ActiveRecord
{
public static function find()
{
return parent::find()->andWhere(['active' => 1]);
}
}
Если мы заходим добавить обычный scope, так как написано в документации (путем наследования от ActiveQuery), метод find будет переопределен.
namespace app\models;
use yii\db\ActiveRecord;
use yii\db\ActiveQuery;
class Comment extends ActiveRecord
{
public static function find()
{
return new CommentQuery(get_called_class());
}
}
class CommentQuery extends ActiveQuery
{
public function active($state = true)
{
return $this->andWhere(['active' => $state]);
}
}
Гибкое решение для default scope
Правильная реализация Default scope — создать свой класс, наследованный от ActiveQuery, и возвращать его в методе find. Также в этом ActiveQuery можно прописывать другие scopes, как это описано в документации.
class MyActiveRecord extends \yii\db\ActiveRecord
{
public static function find()
{
return new MyActiveQuery(get_called_class());
}
}
class MyActiveQuery extends \yii\db\ActiveQuery
{
public function init()
{
$modelClass = $this->modelClass;
$tableName = $modelClass::tableName();
$this->andWhere([$tableName.'.active' => 1]);
parent::init();
}
}
Таким образом в наследуемых MyActiveRecord моделях мы будем иметь доступ ко всем scopes, а если захотим добавить еще default scope или обычные scopes, можно аналогично расширять MyActiveQuery.
UPDATE
Прошу прощения, первая моя статья.
Судя по комментариям у многих возник вопрос, к чему это все я написал и в чем разница.
Если мы прописываем default scope прямо в методе find, а потом в наследуемых классах задаем обычные скоупы в наследниках ActiveQuery, мы перетираем метод find родителя и его условия.
Проблемы возникают в подобных случаях:
- когда нужно задать scopes для моделей, у родителя которых задано условие default scope.
Если делать как в варианте, который я назвал «не правильным», условие default scope будет затерто. - когда у нас большой проект и в нем присутсвуют модули и подмодули.
Часто для специфичных модулей нужны свои scopes и default scope, которые дополняют условия запросов родителей.
Получается, что нужно использовать гибкий вариант всегда, когда создается свой класс ActiveRecord, в котором нужно указать условие запроса к БД по умолчанию.
Т.к. почти всегда найдется другой программист (или вы через пол года), который захочет добавить скоупы в какой-то модели и сделает это как в документации.
Комментарии (17)
zelenin
06.05.2015 20:07собственно не вижу разницы. Больше того, мне кажется это одно и то же. Приведите кейс для понимания разницы.
vism Автор
14.05.2015 11:24В простом случае разницы нет, действительно.
Кейс по шагам:
1) Для всех find к БД нужно добавить условие is_deleted <> 0
2) Создаем свой класс MyActiveRecord в котором прописываем default scope одним из способов
3) В какой-то модели нам понадобились скоупы и мы делаемкак в документацииnamespace app\models; use yii\db\ActiveRecord; use yii\db\ActiveQuery; class Comment extends ActiveRecord { public static function find() { return new CommentQuery(get_called_class()); } } class CommentQuery extends ActiveQuery { public function active($state = true) { return $this->andWhere(['active' => $state]); } }
usualdesigner
06.05.2015 21:40Я согласен, что предложенный в статье метод имеет место быть. Но как же прекрасно выглядел defaultScope(){} в прошлой версии.
Rathil
06.05.2015 23:04Тоже сталкивался с этой проблемой. Долго думал над её решением, предложенный Вами вариант тоже рассматривал, однако ещё до конца не решил как лучше делать (сейчас в отпуске и даю отдохнуть мозгу). Предложенный вариант не удовлетворяет на все 100? дефолт, так как если Вы будете использовать не andWhere, а просто where, лично я считаю, должно заменить все прежние условия, кроме дефолтного! Поэтому я ещё в раздумии, какой костыль изобрести или выбрать.
Unclead
07.05.2015 19:55Можно подключить требуемое условие в момент подготовки запроса к сборке
/** * Выводим все записи у которх статус не в числе невидимых. * * @param \yii\db\QueryBuilder $builder * @return \yii\db\Query */ public function prepare($builder) { $this->andWhere(['not in', $this->statusField, array_diff( $this->hiddenStatuses, $this->exceptedHiddenStatuses )]); return parent::prepare($builder); }
vism Автор
14.05.2015 11:34Интуитивно кажется, что так лучше.
Надо поразмыслить, нет ли подводных камней…
Zhuravljov
07.05.2015 20:11Серебряной пули не существует ).
Дефолтный скоуп — не всегда одно лишь дополнительное условие. Еще иногда нужно дополнить select, join и прочее. Просто скоупы нужно стараться составлять и применять таким образом, чтобы они дополняли базовый запрос но не перекрывали друг друга.
xskif
07.05.2015 01:39А чем вас не устраивает это:
return new CommentQuery(get_called_class)->andWhere(...);vism Автор
14.05.2015 11:36Если будет наследование модели и там будут скоупы (соjтвественно создадим MyCommentQuery), find с условием перетрется
xskif
14.05.2015 14:36+1Если будет наследование модели и для них обоих нужно установить один и тот же default scope, то возможно стоит пересмотреть архитектуру. Конечно Ваше решение подойдет в таком варианте, как DRY, но мне кажется, что это лишняя сложность.
В интернете советуют вообще отказаться от default scope, так как это может вызывать неожиданное поведение, и я с ними согласен. Я использую свои query классы, и если в двух из трех мест приложения мне надо написать один и тот же скоуп — это не WET. В крайнем случае всегда есть фабрика или SOA.
Ну и если так уж нужны default scope в двух наследованных моделях, то почему бы логику default scope не засунуть в общий query класс?
class BaseCommentQuery extends ActiveQuery { public function default () { return $this->andWhere(...); } } class CommentQuery extends BaseCommnetQuery {} class MyCommentQuery extends BaseCommentQuery {} // или extends CommentQuery // class Comment public static function find () { return new CommentQuery(get_called_class())->default(); } // class MyComment public static function find () { return new MyCommentQuery(get_called_class())->default(); }
xskif
14.05.2015 14:42P.S. кстати, если использовать ActiveQuery класс для задания скоупов и для вызова default scope внутри init(), как Вы предложили, — это, как мне кажется, нарушит SRP и SOLID соответственно. Придется лезть в класс и для того чтобы поправить default scope и для того чтобы изменять обычные scope. Комментатор ниже верно говорит:
«Наследник от ActiveQuery определяет список скоупов, но не включает в себя их применение.»
Zhuravljov
07.05.2015 19:38На мой взгляд, предложенный разработчиками вариант выглядит логичнее.
Наследник от ActiveQuery определяет список скоупов, но не включает в себя их применение.
Ekstazi
Default scope на то и «default» чтоб быть по-умолчанию для всех запросов.