Использование SCROPE внутри joinWith

Общие вопросы по использованию второй версии фреймворка. Если не знаете как что-то сделать и это про Yii 2, вам сюда.
Ответить
tlcl444w1plvc6c
Сообщения: 7
Зарегистрирован: 2019.12.18, 19:19

Использование SCROPE внутри joinWith

Сообщение tlcl444w1plvc6c »

Добрый день. Имеется следующий код:

Таблица my_users_table имеет поля: id, name, status
Таблица my_comments_table имеет поля: id, user_id, body, status

Код: Выделить всё

class User extends ActiveRecord {
	public static function tableName() { return 'my_users_table'; }
	public function getComments() { return $this->hasMany( Comments::class, ['user_id' => 'id'] ); }
}
class CommentQuery extends ActiveQuery {
	public function active() {
		return $this->andWhere( ['status' => 1] );
	}
	...
}
class Comment extends ActiveRecord {
	public static function tableName() { return 'my_comments_table''; }
	public static function find() { return new CommentQuery(get_called_class()); }
}
Задача: вывести пользователей у которых есть хотя бы один АКТИВНЫЙ комментарий.

Код: Выделить всё

$query = User::find()->innerJoinWith( [
	'comments' => function( CommentQuery $q ) {
		$q->active();
	}
], false )->active();
echo $query->createCommand()->getRawSql();
Результат работы следующего запроса: SELECT my_users_table.* FROM my_users_table INNER JOIN my_comments_table ON my_comments_table.user_id = my_users_table.id WHERE status = 1
Естественно это не работает, потому что нет чёткого понятия к какой таблице относиться status = 1 выражение.

Как решить эту проблему по красоте без костылей?
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

Вообще-то не должно такой запрос формировать вроде, надо глянуть. Но можете так решить.

Код: Выделить всё

$query = User::find()->innerJoinWith( [
	'comments' => function( CommentQuery $q ) {
		$q->alias('c')->active();
	}
], false )->alias('u')->active();
echo $query->createCommand()->getRawSql();
skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

Код: Выделить всё

$currentTable = Comment::tableName();
return $this->andWhere( ["$currentTable.status" => 1] );

или
$currentTable= $this->modelClass::tableName();
->alias('c') же убрать
Они должны быть "под запретом" в дев гайде проекта, и применяться только в случаях когда по другому никак. то есть когда невозможно без переименования таблиц составить SQL запрос вручную. именно для таких случаев и нужен alias, а не для "удобства", типа меньше кода, или удобнее написать условие "u.id = c.user_id" чем "$userTable.id = $commentTable.user_id"
Применение alias потом очень усложняет наращивание условий запросов, их использование, когда мы еще хотим что-то приджойнить.

А правильные способы - выше *::tableName()
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

skynin писал(а): 2020.02.13, 10:06 А правильные способы - выше *::tableName()
Посмотрел сегодня ActiveQuery, в yii билдер вообще так не умеет с алиасами, так что только вариант с именем таблицы.
Хотя это тоже не очень хороший вариант, вы больше не сможете установить алиас в таких связях.
Оптимально делать так

Код: Выделить всё

class UserQuery extends ActiveQuery {
    private $alias;

    public function withAlias($alias)
    {
        $this->alias = $alias;
        return $this;
    }
    
    public function active() {
	return $this->andWhere( ["$this->alias.active" => 1] );
    }
	...
}


class CommentQuery extends ActiveQuery {
    private $alias;

    public function withAlias($alias)
    {
        $this->alias = $alias;
        return $this;
    }
    
    public function active() {
	return $this->andWhere( ["$this->alias.status" => 1] );
    }
	...
}

Код: Выделить всё

$query = User::find()->alias('u')->innerJoinWith( [
	'comments c' => function( CommentQuery $q ) {
		$q->withAlias('c')->active();
	}
], false )->withAlias('u')->active();
echo $query->createCommand()->getRawSql();
skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

-- Посмотрел сегодня ActiveQuery, в yii билдер вообще так не умеет с алиасами
да, потому что:

-- Хотя это тоже не очень хороший вариант, вы больше не сможете установить алиас в таких связях.
а обычно это не требуется.

Алиас для запросов нужен для соединения таблицы с собой, или для многократного соединения с одной и той же таблицей
А такая потребность возникает очень редко.

-- Оптимально делать так
не оптимально, потому что у вас нет потрбености для этого запроса в множественном соединении.
а значит это - лишний код, который ничего полезного не делает.

и потому что
для того чтобы потом написать ->withAlias('u')->active() нужно искать, знать какой перед этим был алиас, если запрос собирается не в одном месте.

мало того, код лишний, потому что есть getTableNameAndAlias

Итого - вы написали код, который никак не решает задачу, и который был бы не нужен вообще, если бы не использовали alias БЕЗ необходимости :)
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

skynin писал(а): 2020.02.13, 12:30 Алиас для запросов нужен соединения таблицы с собой, или для многократного соединения с одной и той же таблицей
А такая потребность возникает очень редко.
Именно эту проблему он и решает. Кроме того, он более универсальный и покрывает и ваш кейс. Потому что мы можем тупо сделать -withAlias(Comment::tableName())
skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

Задача
вывести пользователей у которых есть хотя бы один АКТИВНЫЙ комментарий.

Берем пользователей - Первая таблица
Джойним комментарии - Другая таблица
Накладываем условие по Другой таблице, по полю с таким же именем как в Первой таблице
"проблема" это указать полное имя этого поля, с именем таблицы?

где здесь требуется переименование таблиц?

-- Кроме того, он более универсальный и покрывает и ваш кейс.
никакой универсальности в нем нет.

а
-- мы можем тупо сделать -withAlias(Comment::tableName())
это просто масло маслянное:
- сначала породили себе проблему
- потом написали костыль
- а потом обязываем обходить этот костыль

Еще раз, следование простому правилу:
Если переименование НЕ требуется в конечном коде SQL запроса
то его не должно быть и в коде на php

избавляет от лишнего кода, который якобы расширяет функционал Yii
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

skynin писал(а): 2020.02.13, 12:39 Задача
вывести пользователей у которых есть хотя бы один АКТИВНЫЙ комментарий.
.....

избавляет от лишнего кода, который якобы расширяет функционал Yii
А если потом понадобится джойнить эту таблицу саму с собой?
Можно кстати, и через getTableNameAndAlias сделать.
skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

yiiliveext писал(а): 2020.02.13, 12:47 А если потом понадобится джойнить эту таблицу саму с собой?
YAGNI («You aren't gonna need it»; с англ. — «Вам это не понадобится») — процесс и принцип проектирования ПО, при котором в качестве основной цели и/или ценности декларируется отказ от избыточной функциональности, — то есть отказ добавления функциональности, в которой нет непосредственной надобности.

Потому что когда понадобится, правильней будет добавить метод к специализированному ActiveQuery, который все это будет обязан спрятать, а не нагружать мозги программиста которому нужно будет писать запросы в бизнес терминах всякой ерундой об алиасах

Даже в этом случае уже нужно бы подумать о

UserActiveQuery::whereActiveComments()

внутри которого и производится и джойн, where

То есть как задача звучит, такой метод и написать

Но необходимость этого метода неизвестна на сейчас, и тоже может подпасть под YAGNI
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

skynin писал(а): 2020.02.13, 12:58
yiiliveext писал(а): 2020.02.13, 12:47 А если потом понадобится джойнить эту таблицу саму с собой?
YAGNI («You aren't gonna need it»; с англ. — «Вам это не понадобится») — процесс и принцип проектирования ПО, при котором в качестве основной цели и/или ценности декларируется отказ от избыточной функциональности, — то есть отказ добавления функциональности, в которой нет непосредственной надобности.

Потому что когда понадобится, правильней будет добавить метод к специализированному ActiveQuery, который все это будет обязан спрятать, а не нагружать мозги программиста которому нужно будет писать запросы в бизнес терминах всякой ерундой об алиасах

Даже в этом случае уже нужно бы подумать о

UserActiveQuery::whereActiveComments()

внутри которого и производится и джойн, where

То есть как задача звучит, такой метод и написать

Но необходимость этого метода неизвестна на сейчас, и тоже может подпасть под YAGNI
Какой принцип нарушает этот код.

Код: Выделить всё

class CommentQuery extends ActiveQuery {
    public function active()
    {
        list(,$alias) = $this->getTableNameAndAlias();
        return $this->andWhere(["$alias.status" => 1]);
    }
	...
}

skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

yiiliveext писал(а): 2020.02.13, 13:08 Какой принцип нарушает этот код.
Никакой.
там в нем все равно
$alias = $tableName;
если не было использование alias

Я же о ненужности его использования, когда это - не нужно.

Но ок, третий вариант к моим двум
Вместо *::tableName() использовать getTableNameAndAlias()

Правда, это правило коснется и наружного кода, а getTableNameAndAlias() это метод ActiveQuery

Снаружи тогда тоже нужно будет думать об алиасах.

Просто потому что какой-то умный очень нафигачил бездумно алиасов

ну ниче, если тех лид не завернул, этот умный выслушает от товарищей по команде про то что это было "удобно", и что может потом нам понадобится. а поверьте на слово, как тех лид на нескольких проектах, я знаю что он услышит :)
то что от меня - я и написал. товарищи выскажутся более просто :)
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

skynin писал(а): 2020.02.13, 13:18
yiiliveext писал(а): 2020.02.13, 13:08 Какой принцип нарушает этот код.
Никакой.
там в нем все равно
$alias = $tableName;
если не было использование alias

Я же о ненужности его использования, когда это - не нужно.

Но ок, третий вариант к моим двум
Вместо *::tableName() использовать getTableNameAndAlias()
Так он же, как и ваши два предыдущих, нарушает ваш первый принцип, по которому запрос после билдера должен выглядеть так, как если бы вы его писали на чистом SQL.
skynin писал(а): 2020.02.13, 13:18 Просто потому что какой-то умный очень нафигачил бездумно алиасов

ну ниче, если тех лид не завернул, этот умный выслушает от товарищей по команде про то что это было "удобно", и что может потом нам понадобится. а поверьте на слово, как тех лид на нескольких проектах, я знаю что он услышит :)
то что от меня - я и написал. товарищи выскажутся более просто :)
Для этого и существуют ревью кода. Это проблема с техлидом/ревьюером. В yii2 и без этого можно столько бездумно нафигачить, что товарищи будут еще долго возмущаться.
skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

-- нарушает ваш первый принцип, по которому запрос после билдера должен выглядеть так, как если бы вы его писали на чистом SQL.
ничуть. вы его значит не так поняли.
-- по которому запрос после билдера должен выглядеть так, как если бы вы его писали на чистом SQL.
нет.
ок, переформулирую его
конечный код на SQL сформированный билдером должен выглядеть так, как если бы он был написан вручную. Или отлчиаться в нюансах.
Из этого и следует - что если в ручном коде не требуется переименование таблиц, то почему оно есть после работы билдера?
Какую цель ставил перед собой программист которая потребовала переименование?

вы просто использовали другой метод получения имени таблицы, с учетом алиаса
но заменить
$table = ::tableName();
на
list($table,$alias) = $this->getTableNameAndAlias();
не проблема, при возникновении необходимости.

об этом сразу и написал
алиасы - убрать
использовать имена таблиц

-- Для этого и существуют ревью кода.
да, и как ревьювер я и пояснил почему "завернул" решение с избыточным применением alias, не по делу.
и тем более решение по изобретению велосипеда для искусственной проблемы, с накладываеним контракта на использование, и т.п.

по другому
была плевая задачка - а какие навороты то понаписаны?
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

skynin писал(а): 2020.02.13, 14:13 ок, переформулирую его
конечный код на SQL сформированный билдером должен выглядеть так, как если бы он был написан вручную. Или отлчиаться в нюансах.
Из этого и следует - что если в ручном коде не требуется переименование таблиц, то почему оно есть после работы билдера?
У вас запрос Comment::find()->active()->all() будет давать такой SQL

Код: Выделить всё

SELECT * FROM `comment` WHERE `comment`.`status` = 1 
С точки зрения того же MySQL это равноценно разбору запроса

Код: Выделить всё

SELECT * FROM `comment` с WHERE с.`status` = 1 
Планы у них будут одинаковые с разным наименованием, которое будет мапиться на реальную таблицу и в том и в другом случае.
Если бы я писал вручную,то запрос выглядел бы так.

Код: Выделить всё

SELECT * FROM `comment` WHERE `status` = 1 
skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

yiiliveext писал(а): 2020.02.13, 14:31 У вас запрос
не у меня, а у вас.

У меня

Код: Выделить всё

Comment::find()->active()->all()
выдаст

Код: Выделить всё

SELECT * FROM `comment` WHERE `status` = 1
Это вы вставили в active() то что приведет к

Код: Выделить всё

SELECT * FROM `comment` WHERE `comment`.`status` = 1
а не я :)

не надо свой код приписывать мне :)

Но вполне ок, более надежен, работает с возможным переименованием.

а у меня с вероятностью 99% был бы
UserActiveQuery::whereActiveComments()

я не люблю по стопицот раз формировать запросы для типичных для системы запросов.
и всем рекомендую использовать возможности - упрятать нюансы хранения данных в ActveQuery, чтобы можно было работать в терминах предметной области, а не схемы БД
yiiliveext писал(а): 2020.02.13, 14:31 Планы у них будут одинаковые с разным наименованием
Да.

Но какая тогда цель применения alias?
Чтобы - что?

Вот это почему

Код: Выделить всё

$query = User::find()->innerJoinWith( [
	'comments' => function( CommentQuery $q ) {
		$q->alias('c')->active();
	}
], false )->alias('u')->active();
а не

Код: Выделить всё

$query = User::find()->innerJoinWith( [
	'comments' => function( CommentQuery $q ) {
		$q->active();
	}
], false )->active();
зачем? с какой целью были применены ->alias('u')?
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

skynin писал(а): 2020.02.13, 14:57
yiiliveext писал(а): 2020.02.13, 14:31 У вас запрос
не у меня, а у вас.

У меня

Код: Выделить всё

Comment::find()->active()->all()
выдаст

Код: Выделить всё

SELECT * FROM `comment` WHERE `status` = 1
Это вы вставили в active() то что приведет к

Код: Выделить всё

SELECT * FROM `comment` WHERE `comment`.`status` = 1
а не я :)

не надо свой код приписывать мне :)
У вас будет абсолютно то же. Имя таблицы вообще в вашем варианте всегда добавляется, надо оно там или нет. Зачем эта избыточность?

Но какая тогда цель применения alias?
Чтобы - что?

Вот это почему

Код: Выделить всё

$query = User::find()->innerJoinWith( [
	'comments' => function( CommentQuery $q ) {
		$q->alias('c')->active();
	}
], false )->alias('u')->active();
а не

Код: Выделить всё

$query = User::find()->innerJoinWith( [
	'comments' => function( CommentQuery $q ) {
		$q->active();
	}
], false )->active();
зачем? с какой целью были применены ->alias('u')?
Это вообще нерабочий вариант, писал навскидку, о его неработоспособности написал сразу, как посмотрел класс ActiveQuery. На память я не помню всех особенностей разных библиотек/фреймворков.
skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

-- Зачем эта избыточность?
вот я и спрашиваю, зачем эта избыточность с alias?

-- Имя таблицы вообще в вашем варианте всегда добавляется, надо оно там или нет.
В моем варианте - добавляйте имя таблицы когда надо. Добавлять вот так.
А когда не надо то и не добавляйте.

а добавлять всегда - это ваш вариант.

И - речь об избыточности в php коде, а не SQL запросе.
а правило придумано для того чтобы его было легко запомнить. а потому ему легко было следовать.
Паравило об использовании alias: используй alias тогда когда невозможно написать SQL вручную БЕЗ переименования таблиц
Иначе - не используй, за исключением уникальных случаев которые будут рационально обоснованным исключением из этого правила.

а то что вы не поняли что для чего в моем варианте, это да :)

-- На память я не помню всех особенностей
ну я и написал, "избыточность убрать"
потому что -
1. не нужно
2. уже есть

с каким пунктом вы тогда дискутируете?

-- Это вообще нерабочий вариант,
да

потому что у меня применяется расширенный ActiveQuery
в котором есть вот такие два метода

public function columnAliases(array $columnAlias = [])
private function disambiguateDone()

то есть снаружи при использовании, если уж очень не хочется ставить самому, просто указывается какие колонки нужны с полным именем
а private function disambiguateDone() потому и в private что вся магия упрятана

и по опыту использования... нужно то было пару раз, и только потому что понатыканы alias('c') и alias('u') в запросе с 5тью джойнами
и с where по id которая есть в каждой из 6ти таблиц

то есть проблему эту я копал

но, как ниже написано:
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

skynin писал(а): 2020.02.13, 15:13 -- Имя таблицы вообще в вашем варианте всегда добавляется, надо оно там или нет.
В моем варианте - добавляйте имя таблицы когда надо. Добавлять вот так.
а добавлять всегда - это ваш варинат.
А когда не надо то и не добавляйте.
Вы шутите?
В моем варианте

Код: Выделить всё

class CommentQuery extends ActiveQuery {
    private $alias;

    public function withAlias($alias)
    {
        $this->alias = $alias;
        return $this;
    }
    
    public function active() {
	return $this->andWhere( ["$this->alias.status" => 1] );
    }
	...
}
При таком запросе

Код: Выделить всё

Comment::find()->active()->all()
Будет сформирован такой SQL

Код: Выделить всё

SELECT * FROM `comment` WHERE `status` = 1 
В вашем варианте

Код: Выделить всё

class CommentQuery extends ActiveQuery {
    public function active() {
        $currentTable = $this->modelClass::tableName();
	return $this->andWhere( ["$currentTable.status" => 1] );
    }
	...
}
При таком запросе

Код: Выделить всё

Comment::find()->active()->all()
Будет сформирован такой SQL

Код: Выделить всё

SELECT * FROM `comment` WHERE `comment`.`status` = 1 
Если для вас это не очевидно, то мне страшно за те проекты, где вы были техлидом.
И да, последний запрос ничем не отличается от запроса с алиасом. Нет никакого переименование таблиц, есть именование их экземпляров.
Наличие метода withAlias() никак не обязывает его использовать, а только позволяет писать через билдер в точности такие запросы, как вы бы писали вручную. И даже отсутствия этого метода никак не помешает натыкать алиасов нативным методом alias() где нужно и где ненужно, если вы не будете это контролировать.
И - речь об избыточности в php коде, а не SQL запросе.
а правило придумано для того чтобы его было легко запомнить. а потому ему легко было следовать.
Паравило об использовании alias: используй alias тогда когда невозможно написать SQL вручную БЕЗ переименования таблиц
Иначе - не используй, за исключением уникальных случаев которые будут рационально обоснованным исключением из этого правила.

а то что вы не поняли что для чего в моем варианте, это да :)
с каким пунктом вы тогда дискутируете?
С таким подходом нужно выкинуть половину методов и классов фреймворка.
Не надо тупо всегда в точности следовать всем паттернам и правилам, иногда это неоправданно.
skynin
Сообщения: 400
Зарегистрирован: 2017.12.12, 10:09

Re: Использование SCROPE внутри joinWith

Сообщение skynin »

-- Если для вас это не очевидно, то мне страшно за те проекты, где вы были техлидом.
например текущий проект два раза уже проходил стороннюю техническую экспертизу.
пока заказчики довольны ее результатами :)

как и в предыдущих проектах.

так что меня не смущают форумные оценщики :)

-- С таким подходом нужно выкинуть половину методов и классов фреймворка.
подход "обычного программиста Yii" да, мне известен, перевидел тьмы вот такого лишнего кода, копипасты формирования одних и тех же запросов, и т.п. вызванные привычкой работать в одиночку над CRUD приложениями.

-- Наличие метода withAlias() никак не обязывает его использовать, а только позволяет писать через билдер в точности такие запросы, как вы бы писали вручную.
добавление этого метода для задачи в топике - избыточно.
решает же он те будущие задачи - криво.

а вы так и не поняли что соответствие ручного кода и сгенерированого не является целью.

-- В вашем варианте
мой вариант неиспользования alias написан для решения конкретной задачи, с учетом предполагаемого уровня программиста который с ней столкнулся.

И настоящий мой вариант я озвучил, но он неизвестно ли нужен, и понятнее ли.

-- И даже отсутствия этого метода никак не помешает натыкать алиасов нативным методом alias()
это контролируется легче чем вы думаете.

Например я недавно уволил спорщика типа вас. утомил просто вот такими бестолковыми возражениями как ваши :)
ну а своим кодом=бредотворчеством и велосипедостроением - команду. и это конечно было важнее моей усталости.

так что да, переживайте сильнее за те проекты куда меня брали тех лидом :D
Не желайте странного, и не будет у вас головной боли чтобы достичь этого странного.
Тем более что окажется что оно вам и не нужно было, странное это.
yiiliveext
Сообщения: 910
Зарегистрирован: 2019.08.13, 01:49

Re: Использование SCROPE внутри joinWith

Сообщение yiiliveext »

Отвечу вам вашой же подписью.
Неврубающийся не может опознать врубающегося.
Удачи в проектах.
Ответить