ActiveRecord и значения связанных таблиц

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

ActiveRecord и значения связанных таблиц

Сообщение GHopper »

Здравствуйте.

Мой уровень знаний Yii2 - нулевой. Только начал изучать. Сразу прошу прощения за, возможно, глупые вопросы.

Имеем структуру из трех связанных таблиц Федеральный округ->Регион->Город:
Изображение

Имеется объект ActiveRecord на базе таблицы City:

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

class City extends \yii\db\ActiveRecord
{
    public $region_name;

    public $fdistrict_id;
    public $fdistrict_name;

...

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getRegion()
    {
        return $this->hasOne(Region::className(), ['id' => 'region_id']);
    }

    public function getFdistrict()
    {
        return $this->hasOne(Fdistrict::className(), ['id' => 'fdistrict_id'])->via('region');
    }
}
Собственно вопросы:
1. Хочу допилить стандартный CRUD, чтобы можно было при добавлении города фильтровать регионы по федеральному округу.
views/city/_form.php:

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

...
<?= $form->field($model, 'fdistrict_id')->dropDownList(
    ArrayHelper::map(
        Fdistrict::find()->select(['name', 'id'])->orderBy('name')->all(),
            'id',
            'name'),
        ['class' => 'form-control inline-block']) 
?>
...
Чтобы эта конструкция работала, приходится перед рендерингом данного шаблона, руками задавать поле fdistict_id:

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

    protected function findModel($id)
    {
        if (($model = City::findOne($id)) !== null) {
            $model->fdistrict_id = $model->fdistrict->id; // <---- here the trick
            return $model;
        } else {
            throw new NotFoundHttpException('The requested page does not exist.');
        }
    }
И меня почему-то он несколько огорчает - хочу построить самостоятельную модель, которая бы сама могла подгрузить все необходимые значения связанных таблиц. Как этого добиться? Пробовал добавлять этот код в конструктор и в init(), но результата не получил.

2. Если заменить
<?= $form->field($model, 'fdistrict_id')->dropDownList(
на
<?= $form->field($model, 'fdistrict')->dropDownList(
, то все работает исправно без ручного указания
$model->fdistrict_id = $model->fdistrict->id;
. И я не понимаю, как он находит нужный член класса (элемент массива в данном случае).

3. Сформировал следующий запрос для провайдера:

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

$query = City::find()
            ->select(['city.*', 'fdistrict.id AS fdistrict_id', 'region.name AS region_name', 'fdistrict.name AS fdistrict_name'])
            ->leftJoin('region', 'region.id = city.region_id')
            ->leftJoin('fdistrict', 'fdistrict.id = region.fdistrict_id')
            ->with(['region', 'fdistrict']);
Заметил, что сперва фреймворк выбирает данные со всеми join, а потом вызывает еще два запроса к таблицам fegion и fdistrict с условиями id IN (...) На голом PHP я бы это все сделал в один запрос. Как это можно реализовать на ActiveRecord одним запросом?

4. После того, как разберусь с моделью City, приступлю к освоению динамичных форм на ajax (фильтр регионов при смене федерального округа). Даже не представляю как это делается в Yii2, буду рад ссылке на хороший мануал. Сэкономите мне пару часов жизни )
Аватара пользователя
Alexum
Сообщения: 683
Зарегистрирован: 2016.09.26, 10:00

Re: ActiveRecord и значения связанных таблиц

Сообщение Alexum »

GHopper писал(а): 2017.06.05, 11:48 1. Хочу допилить стандартный CRUD, чтобы можно было при добавлении города фильтровать регионы по федеральному округу.
views/city/_form.php:
Как вариант, можно воспользоваться этим расширением http://demos.krajee.com/widget-details/depdrop. Либо http://demos.krajee.com/widget-details/select2. Во втором случае делаете отдельный экшен в контроллере для получения списка городов по переданному id округа. Вешаете обработчик на событие 'select' на select с округами и подтягиваете через Ajax список городов в select с городами. Получаете почти тот же самый depdrop :). Как пример, можно посмотреть ролик: https://www.youtube.com/watch?v=ZepxKw8 ... 20VxLx2mkF (там вариант вообще без расширений, с обычным dropDownList).
GHopper писал(а): 2017.06.05, 11:48 И меня почему-то он несколько огорчает - хочу построить самостоятельную модель, которая бы сама могла подгрузить все необходимые значения связанных таблиц.
Хотите - создавайте отдельную модель для формы - это нормально.
GHopper писал(а): 2017.06.05, 11:48 2. Если заменить
<?= $form->field($model, 'fdistrict_id')->dropDownList(
на
<?= $form->field($model, 'fdistrict')->dropDownList(
, то все работает исправно без ручного указания
$model->fdistrict_id = $model->fdistrict->id;
. И я не понимаю, как он находит нужный член класса (элемент массива в данном случае).
$model->fdistrict->id - так можно обратиться к свойству связанного объекта (используя связь getFdistrict() в модели), а $model->fdistrict соответственно вернёт связанный объект (или массив объектов, если связь hasMany). Однако в "голом" виде - это весьма опасное обращение и необходима проверка, т.к. $model->fdistrict может не вернуть объект и поймаете исключение.
GHopper писал(а): 2017.06.05, 11:48 3. Сформировал следующий запрос для провайдера:

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

$query = City::find()
            ->select(['city.*', 'fdistrict.id AS fdistrict_id', 'region.name AS region_name', 'fdistrict.name AS fdistrict_name'])
            ->leftJoin('region', 'region.id = city.region_id')
            ->leftJoin('fdistrict', 'fdistrict.id = region.fdistrict_id')
            ->with(['region', 'fdistrict']);
Заметил, что сперва фреймворк выбирает данные со всеми join, а потом вызывает еще два запроса к таблицам fegion и fdistrict с условиями id IN (...) На голом PHP я бы это все сделал в один запрос. Как это можно реализовать на ActiveRecord одним запросом?
1) Одним запросом - никак. Грубо говоря идея - первым запросом получить данные для основной модели, получить id связанных записей. Для связанных моделей данные вытаскиваются отдельными запросами по ключам. Такой вариант может быть более выигрышным по производительности.
2) Зачем вам такой сложный запрос? City::find()->joinWith(['region reg','fdistrict fd']) этого хватит для Active-провайдера с возможностью навесить фильтры и сортировку на связанные поля. Если фильтры и сортировка по связанным данным не нужны, то City::find()->with(['region','fdistrict']) исключительно ради жадной загрузки.
GHopper писал(а): 2017.06.05, 11:48 4. После того, как разберусь с моделью City, приступлю к освоению динамичных форм на ajax (фильтр регионов при смене федерального округа). Даже не представляю как это делается в Yii2, буду рад ссылке на хороший мануал. Сэкономите мне пару часов жизни )
Ответил в начале.
GHopper
Сообщения: 83
Зарегистрирован: 2017.06.05, 10:53

Re: ActiveRecord и значения связанных таблиц

Сообщение GHopper »

Alexum писал(а): 2017.06.05, 13:40 Как пример, можно посмотреть ролик: https://www.youtube.com/watch?v=ZepxKw8 ... 20VxLx2mkF (там вариант вообще без расширений, с обычным dropDownList).
Отличный ролик, спасибо. Бегло пробежался - то, что я и хочу реализовать.
Alexum писал(а): 2017.06.05, 13:40 $model->fdistrict->id - так можно обратиться к свойству связанного объекта (используя связь getFdistrict() в модели), а $model->fdistrict соответственно вернёт связанный объект (или массив объектов, если связь hasMany). Однако в "голом" виде - это весьма опасное обращение и необходима проверка, т.к. $model->fdistrict может не вернуть объект и поймаете исключение.
А как мне сделать автоматическое заполнение этого поля? Т.е. после того, как модель City создается, она сама должна присваивать соответствующее значение для члена fdistrict_id, который является инденификатором записи в связанной таблице. Мне бы хотелось чтобы модель это делала сама, проверяла все исключения. А я в проекте только вызываю $mod = City::findOne($id) и знаю, что в полученной модели уже есть все мне необходимое, в частности $mod->fdistrict_id.
Alexum писал(а): 2017.06.05, 13:40 2) Зачем вам такой сложный запрос? City::find()->joinWith(['region reg','fdistrict fd']) этого хватит для Active-провайдера с возможностью навесить фильтры и сортировку на связанные поля. Если фильтры и сортировка по связанным данным не нужны, то City::find()->with(['region','fdistrict']) исключительно ради жадной загрузки.
Попытался оптимизировать:

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

        $query = City::find()
            ->select(['city.*', 'fdistrict.id AS fdistrict_id', 'region.name AS region_name', 'fdistrict.name AS fdistrict_name'])
            ->joinWith(['region', 'fdistrict']);
Но мне это все-равно не нравится. Например, код из шаблона /views/city/view.php

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

    <?= DetailView::widget([
        'model' => $model,
        'attributes' => [
            'id',
            'fdistrict.name',
            'region.name',
            'name',
        ],
    ]) ?>
Отправляет три (три, Карл!) запроса к БД. Вместо одного. А если на странице будет 100 таких связанных элементов? А если эту страницу запросит 1000 пользователей в минуту? Про кеширование мы пока не говорим, я пытаюсь оптимизировать работу с БД. Это опять та-же самая ситуация со связями в модели - rdistrict.name и region.name подгружаются в процессе работы, а мне бы хотелось жадную загрузку всех связанных таблиц в одном запросе. Это же уменьшит кол-во обращений к БД на 300%, разве не стоит об этом подумать?!
Аватара пользователя
Alexum
Сообщения: 683
Зарегистрирован: 2016.09.26, 10:00

Re: ActiveRecord и значения связанных таблиц

Сообщение Alexum »

Что вы так привязались к количеству обращений? За данными делается один запрос к каждой таблице (при жадной загрузке). Если у вас 100 объектов основной модели имеют связь к одному и тому же городу, то из таблицы городов вернётся всего одна запись с данными. Т.е. получается один запрос за 100 строками в основной таблице и 1 лёгкий (по primary_key) запрос за одной записью с городом (вместо JOINа на 100 записей, который потащит за собой задублированные данные). А если используется with то вообще join-ов не будет. Если принципиально хотите уложиться в один запрос - ваш выбор SqlDataProvider и вручную собранный запрос.

PS. В плане уменьшения запросов к БД - можно кэшировать схемы.

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

'components' => [
        'db' => [
	     ...
            'enableSchemaCache' => true,
GHopper
Сообщения: 83
Зарегистрирован: 2017.06.05, 10:53

Re: ActiveRecord и значения связанных таблиц

Сообщение GHopper »

Эврика! Придумал как избавиться от ручного присвоения членам модели значений связанных таблиц - нужно прописать соответствующие геттеры, т.е. вместо

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

$model->related_table_field_value = $model->related_table->field;
Мы просто создаем член-геттер, который возвращает требуемое значение по запросу:

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

public function getRelatedTableFieldValue()
{
	return isset($this->related_table_field) ? $this->related_table->field : null;
}

$model->related_table_field_value = $model->related_table->field;
Только теперь возник другой вопрос - как нужно назвать геттер, чтобы он срабатывал при запросе related_table_field?

Ну и по поводу запросов, SqlDataProvider решает мою проблему, только вот с ним не столь удобно работать - приходится самому формировать строку запроса, следить за соблюдением sql-синтаксиса и безопасностью. Слава богу с сегодняшним релизом добавили автоматический подсчет count - одной заботой уже меньше.
mkramer
Сообщения: 531
Зарегистрирован: 2014.12.14, 13:02

Re: ActiveRecord и значения связанных таблиц

Сообщение mkramer »

GHopper, почитайте ещё раз доку по ActiveRecord. Зачем вы вручную поля из БД в классе AR прописываете? Почитайте, как связи делать. http://www.yiiframework.com/doc-2.0/gui ... ecord.html - подробнейше всё описано. Если с английским языком проблемы: https://nix-tips.ru/yii2-api-guides/gui ... ecord.html. С этим фреймворком очень редко возникают ситуации, когда официальная документация не содержала бы ответа.
GHopper
Сообщения: 83
Зарегистрирован: 2017.06.05, 10:53

Re: ActiveRecord и значения связанных таблиц

Сообщение GHopper »

mkramer писал(а): 2017.06.07, 09:36 GHopper, почитайте ещё раз доку по ActiveRecord. Зачем вы вручную поля из БД в классе AR прописываете? Почитайте, как связи делать. http://www.yiiframework.com/doc-2.0/gui ... ecord.html - подробнейше всё описано. Если с английским языком проблемы: https://nix-tips.ru/yii2-api-guides/gui ... ecord.html. С этим фреймворком очень редко возникают ситуации, когда официальная документация не содержала бы ответа.
Перечитал. Заново. Плюс пробежался по DAO и Query Builder. Вник и выждал некоторое время.
Теперь возвращаюсь к своему запросу

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

$query = City::find()
            ->select(['city.*', 'fdistrict.id AS fdistrict_id', 'region.name AS region_name', 'fdistrict.name AS fdistrict_name'])
            ->joinWith(['region', 'fdistrict']);
и не понимаю как мне автоматически присвоить значение City::fdistrict_id, чтобы в последствии передать его, например, в конструктор формы:

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

...
$form->field($model, 'fdistrict_id')->dropDownList(
...
(запись fdistrict->id не принимается, хотя, как выяснилось, fdistrict[id] работает должным образом; но это уже отступление)

Есть две идеи:
1. Присвоить руками после создания объекта City:

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

$model = City::findOne($id);
$model->fdistrict_id = $model->fdistrict->id;
Мне совесть не позволит такой код заказчику отдать, ибо я убежден, что это дно ООП.

2. Создать метод-геттер, который бы возвращал нужное значение:

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

public function getFdistrict_Id()
{
	return isset($this->fdistrict) ? $this->fdistrict->id : null;
}
(Как его все-таки правильно нужно называть? После символа '_' заглавная буква?)
Я убежден, что теоретически это должно работать, но тогда придется избавиться от члена класса City

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

class City extends \yii\db\ActiveRecord
{
...
  public $fdistrict_id;
...
}
В таком случае непонятно, будет-ли связанный код:

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

    public function search($params)
    {
         $query = City::find()
            ->select(['city.*', 'region.name AS region_name', 'fdistrict.id AS ffdistrict_id', 'fdistrict.name AS fdistrict_name'])
            ->joinWith(['fdistrict']);

        // add conditions that should always apply here

        $dataProvider = new ActiveDataProvider([
            'query' => $query,
            'sort' => [
                'defaultOrder' => ['name' => SORT_ASC],
                'attributes' => [
                    'name',
                    'region_name',
                    'fdistrict_id' => [
                        'asc' => ['fdistrict_name' => SORT_ASC],
                        'desc' => ['fdistrict_name' => SORT_DESC],
                    ]
                ]
            ],
        ]);

        $this->load($params);

        if (!$this->validate()) {
            // uncomment the following line if you do not want to return any records when validation fails
            // $query->where('0=1');
            return $dataProvider;
        }

        // grid filtering conditions
        $query->andFilterWhere([
            '{{%city}}.id' => $this->id,
            '{{%region}}.id' => $this->region_id,
            '{{%fdistrict}}.id' => $this->fdistrict_id
        ]);

        $query->andFilterWhere(['like', '{{%city}}.name', $this->name])
            ->andFilterWhere(['like', '{{%region}}.name', $this->region_name]);

        return $dataProvider;
    }
 
выполняться должным образом, т.к. в нем явно прописаны члены базового класса fdistrict_id, fdistrict_name, region_name

В общем, официальная документация не помогла мне найти ответы на свои вопросы. Может я тупой? Ну тогда помогите, в формате "для тупых" разъясните где я заблуждаюсь. Буду очень благодарен.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: ActiveRecord и значения связанных таблиц

Сообщение ElisDN »

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

public $region_name;

public function search($params)
{
    $query = City::find()->alias('c')->joinWith(['fdistrict d', 'fregion r']);

    $dataProvider = new ActiveDataProvider([
        'query' => $query,
        'sort' => [
            'defaultOrder' => ['name' => SORT_ASC],
            'attributes' => [
                'name' => [
                    'asc' => ['c.name' => SORT_ASC],
                    'desc' => ['c.name' => SORT_DESC],
                ],
                'fregion_id' => [
                    'asc' => ['r.name' => SORT_ASC],
                    'desc' => ['r.name' => SORT_DESC],
                ],
                'fdistrict_id' => [
                    'asc' => ['d.name' => SORT_ASC],
                    'desc' => ['d.name' => SORT_DESC],
                ],
            ],
        ],
    ]);

    $this->load($params);

    $query->andFilterWhere([
        'c.id' => $this->id,
        'r.id' => $this->region_id,
        'd.id' => $this->fdistrict_id
    ]);

    $query->andFilterWhere(['like', 'c.name', $this->name])
        ->andFilterWhere(['like', 'r.name', $this->region_name]);

    return $dataProvider;
}

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

<?= GridView::widget([
    'columns' => [
        'id',
        'name',
        'fregion_id',
        [
            'attribute' => 'fregion_name',
            'value' => 'fregion.name',
        ],
        [
            'attribute' => 'fdistinct_id',
            'value' => 'fdistinct.name',
        ],
    ],
]) ?>
GHopper
Сообщения: 83
Зарегистрирован: 2017.06.05, 10:53

Re: ActiveRecord и значения связанных таблиц

Сообщение GHopper »

ElisDN писал(а): 2017.06.08, 10:26

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

public $region_name;

public function search($params)
{
    $query = City::find()->alias('c')->joinWith(['fdistrict d', 'fregion r']);

    $dataProvider = new ActiveDataProvider([
        'query' => $query,
        'sort' => [
            'defaultOrder' => ['name' => SORT_ASC],
            'attributes' => [
                'name' => [
                    'asc' => ['c.name' => SORT_ASC],
                    'desc' => ['c.name' => SORT_DESC],
                ],
                'fregion_id' => [
                    'asc' => ['r.name' => SORT_ASC],
                    'desc' => ['r.name' => SORT_DESC],
                ],
                'fdistrict_id' => [
                    'asc' => ['d.name' => SORT_ASC],
                    'desc' => ['d.name' => SORT_DESC],
                ],
            ],
        ],
    ]);

    $this->load($params);

    $query->andFilterWhere([
        'c.id' => $this->id,
        'r.id' => $this->region_id,
        'd.id' => $this->fdistrict_id
    ]);

    $query->andFilterWhere(['like', 'c.name', $this->name])
        ->andFilterWhere(['like', 'r.name', $this->region_name]);

    return $dataProvider;
}

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

<?= GridView::widget([
    'columns' => [
        'id',
        'name',
        'fregion_id',
        [
            'attribute' => 'fregion_name',
            'value' => 'fregion.name',
        ],
        [
            'attribute' => 'fdistinct_id',
            'value' => 'fdistinct.name',
        ],
    ],
]) ?>
Дмитрий спасибо за ответ. В вашем случае region_id, fdistrict_id, region_name это вообще что? Т.е. в запросе их нет, как члены класса они нигде не указаны и значения им никакие не присваиваются. И вообще зачем в данном примере алиасы? Мы же не джойним таблицы с одинаковыми именами и не замыкаем их саму на себя - вполне себе отлично работает вариант select region.name, fdistrict.name. Ну разве что для красоты, но в данном случае это больше усложняет понимание.

И опять-же, я не могу указать

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

...
<?= $form->field($model, 'r.name')->dropDownList(
    ArrayHelper::map(
        Fdistrict::find()->select(['name', 'id'])->orderBy('name')->all(),
            'id',
            'name'),
        ['class' => 'form-control inline-block']) 
?>
...
т.к. Yii2 выдаст ошибку. Т.е. мы приходим к тому, с чего и начали - в классе City нужен член, который бы принимал значение из поля связанной таблицы и весь вопрос в том, как это значение ему присвоить. Выше я приводи два ответа на свой вопрос, но оба имеют недостатки.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: ActiveRecord и значения связанных таблиц

Сообщение ElisDN »

GHopper писал(а): 2017.06.08, 17:31 В вашем случае region_id, fdistrict_id, region_name это вообще что? Т.е. в запросе их нет, как члены класса они нигде не указаны и значения им никакие не присваиваются.
Это поля из базы, по которым сделаны ваши связи:

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

public function getRegion()
{
    return $this->hasOne(Region::className(), ['id' => 'region_id']);
}

public function getFdistrict()
{
    return $this->hasOne(Fdistrict::className(), ['id' => 'fdistrict_id'])->via('region');
}
GHopper писал(а): 2017.06.08, 17:31 И вообще зачем в данном примере алиасы? Мы же не джойним таблицы с одинаковыми именами и не замыкаем их саму на себя
Алиасы чтобы 'c.name' вместо вашего '{{%city}}.name' указывать.
GHopper писал(а): 2017.06.08, 17:31 вполне себе отлично работает вариант select fregion.name, fdistrict.name.
Без ваших алиасов это вываливает ошибку "field name is ambiguous".
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: ActiveRecord и значения связанных таблиц

Сообщение ElisDN »

Хотя для промежуточной связи нужно немного изменить:

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

public $region_name;

public function search($params)
{
    $query = City::find()->alias('c')->joinWith(['fdistrict d', 'fdistrict.fregion r']);
    ...
}

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

<?= GridView::widget([
    'columns' => [
        'id',
        'name',
        'fregion_id',
        [
            'attribute' => 'fregion_name',
            'value' => 'fdistrict.fregion.name',
        ],
        [
            'attribute' => 'fdistinct_id',
            'value' => 'fdistinct.name',
        ],
    ],
]) ?>
Ответить