Извлечение данных из таблицы соединений в Yii2

Я пытаюсь получить данные из таблицы соединений в Yii2 без дополнительного запроса. У меня есть две модели (User, Group), связанные через таблицу соединений (user_group). В таблице user_group я хочу сохранить дополнительные данные (флаг администратора,...) для этого отношения.

  • Какой лучший способ добавить данные в таблицу соединений? Метод ссылки принимает параметр extraColumns, но я не могу понять, как это работает.

  • Какой лучший способ получить эти данные? Я написал дополнительный запрос, чтобы получить значения из таблицы соединений. Должен быть более чистый способ сделать это?!

FYI, вот как я определил отношение в моделях:

Group.php

public function getUsers() {
    return $this->hasMany(User::className(), ['id' => 'user_id'])
        ->viaTable('user_group', ['group_id' => 'id']);
}

User.php

public function getGroups() {
    return $this->hasMany(Group::className(), ['id' => 'group_id'])
        ->viaTable('user_group', ['user_id' => 'id']);
}

Ответ 1

Короче. Используя ActiveRecord для таблицы соединений, как вы сказали, это ИМХО правильно, потому что вы можете настроить via() для использования существующего ActiveRecord. Это позволяет использовать метод Yii link() для создания элементов в таблице соединений при одновременном добавлении данных (например, вашего флага администратора).


В официальном руководстве Yii Guide 2.0 указывается два способа использования таблицы соединений: использование viaTable() и использование via() (см. здесь). В то время как первое ожидает, что имя таблицы переходов как параметр, последний ожидает имя отношения как параметра.

Если вам нужен доступ к данным внутри таблицы переходов, я бы использовал ActiveRecord для таблицы соединений, как вы предложили, и используйте via():

class User extends ActiveRecord
{
    public function getUserGroups() {
        // one-to-many
        return $this->hasMany(UserGroup::className(), ['user_id' => 'id']);
    }
}

class Group extends ActiveRecord
{
    public function getUserGroups() {
        // one-to-many
        return $this->hasMany(UserGroup::className(), ['group_id' => 'id']);
    }

    public function getUsers()
    {
        // many-to-many: uses userGroups relation above which uses an ActiveRecord class
        return $this->hasMany(User::className(), ['id' => 'user_id'])
            ->via('userGroups');
    }
}

class UserGroup extends ActiveRecord
{
    public function getUser() {
        // one-to-one
        return $this->hasOne(User::className(), ['id' => 'user_id']);
    }

    public function getGroup() {
        // one-to-one
        return $this->hasOne(Group::className(), ['id' => 'userh_id']);
    }
}

Таким образом вы можете получить данные таблицы соединений без дополнительных запросов, используя отношение userGroups (например, с любым другим отношением один-ко-многим):

$group = Group::find()->where(['id' => $id])->with('userGroups.user')->one();
// --> 3 queries: find group, find user_group, find user
// $group->userGroups contains data of the junction table, for example:
$isAdmin = $group->userGroups[0]->adminFlag
// and the user is also fetched:
$userName = $group->userGroups[0]->user->name

Все это можно сделать, используя отношение hasMany. Поэтому вы можете спросить, почему вы должны объявлять отношение "многие ко многим", используя via(): потому что вы можете использовать метод Yii link() для создания элементов в таблице соединений:

$userGroup = new UserGroup();
// load data from form into $userGroup and validate
if ($userGroup->load(Yii::$app->request->post()) && $userGroup->validate()) {
    // all data in $userGroup is valid
    // --> create item in junction table incl. additional data
    $group->link('users', $user, $userGroup->getDirtyAttributes())
}

Ответ 2

Поскольку я не получил ответа почти 14 дней, я опубликую, как я решил эту проблему. Это не совсем то, что я имел в виду, но он работает, этого достаточно на данный момент. Итак... это то, что я сделал:

  • Добавлена ​​модель UserGroup для таблицы соединений
  • Добавлено отношение к группе

    public function getUserGroups()
    {
        return $this->hasMany(UserGroup::className(), ['user_id' => 'id']);
    }
    
  • Присоединился к UserGroup в моей функции поисковой модели

    $query = Group::find()->where('id =' . $id)->with('users')->with('userGroups');
    

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

Я подумал о том, чтобы сначала расширить функцию построения Yii2 - это может быть лучшим способом решить эту проблему. Но так как я еще не знаю Yii2, я решил не делать этого.

Пожалуйста, дайте мне знать, если у вас есть лучшее решение.

Ответ 3

Я не знаю наверняка, это лучшее решение. Но для моего проекта это будет хорошо на данный момент:)

1) Левое соединение

Добавить новый атрибут класса в User model public $flag;. Добавьте две строки к своему основному отношению, но не удаляйте viaTable, это может (и должно) остаться.

public function getUsers()
{
    return $this->hasMany(User::className(), ['id' => 'user_id'])
        ->viaTable('user_group', ['group_id' => 'id'])
        ->leftJoin('user_group', '{{user}}.id=user_id')
        ->select('{{user}}.*, flag') //or all ->select('*');
}

leftJoin позволяет выбрать данные из таблицы соединений и select, чтобы настроить возвращаемые столбцы. Помните, что viaTable должен оставаться, потому что link() полагается на него.

2) запрос подвыбора

Добавить новый атрибут класса в User model public $flag;

И в Group измененная модель getUsers():

public function getUsers()
{
    return $this->hasMany(User::className(), ['id' => 'user_id'])
        ->viaTable('user_group', ['group_id' => 'id'])
        ->select('*, (SELECT flag FROM user_group WHERE group_id='.$this->id.' AND user_id=user.id LIMIT 1) as flag');
}

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

3) Соотношения условий

Другим вариантом является создание только одного отношения для администраторов:

// Select all users
public function getUsers() { .. }

// Select only admins (users with specific flag )
public function getAdmins()
{
    return $this->hasMany(User::className(), ['id' => 'user_id'])
        ->viaTable('user_group', ['group_id' => 'id'],
        function($q){
             return $q->andWhere([ 'flag' => 'ADMIN' ]); 
        });
}

$Group->admins - получить пользователей с определенным флагом администратора. Но это решение не добавляет атрибут $flag. Вам нужно знать, когда вы выбираете только администраторов и всех пользователей. Нижняя сторона: вам нужно создать отдельное отношение для каждого значения флага.


Ваше решение с использованием отдельной модели UserGroup по-прежнему является более гибким и универсальным для всех случаев. Как вы можете добавить валидацию и базовые материалы ActiveRecord. Эти решения - более одностороннее направление - чтобы получить материал.

Ответ 4

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

foreach ($parentModel->relatedModels as $childModel)
{
    $childModel->junction_table_column1;
    $childModel->junction_table_column2;
    ....
}

Для получения дополнительной информации, пожалуйста, взгляните на Расширение атрибутов таблицы расширения Yii2

Спасибо.