Laravel Многие-ко-многим (в той же таблице / модели пользователей): области запросов для включения связанных с указанным пользователем - PullRequest
2 голосов
/ 26 марта 2020

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

/**
 * Get the users that are blocked by $this user.
 *
 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 */
public function blockedUsers()
{
    return $this->belongsToMany(User::class, 'ignore_lists', 'user_id', 'blocked_user_id');
}

/**
 * Get the users that blocked $this user.
 *
 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 */
public function blockedByUsers()
{
    return $this->belongsToMany(User::class, 'ignore_lists', 'blocked_user_id', 'user_id');
}

(ignore_lists - это сводная таблица, и она имеет id, user_id, 'blocked_user_id' столбцы)

Я хочу создать следующие Области запросов :

1) Чтобы включить пользователей, которые являются заблокирован указанным пользователем ($id):

/**
 * Scope a query to only include users that are blocked by the specified user.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 * @param $id
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeAreBlockedBy($query, $id)
{
    // How to do this? :)
}

Пример использования: User::areBlockedBy(auth()->id())->where('verified', 1)->get();

2) Включить пользователей не заблокированы указанным пользователем ($id):

/**
 * Scope a query to only include users that are not blocked by the specified user.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 * @param $id
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeAreNotBlockedBy($query, $id)
{
    // How to do this? :)
}

Пример использования: User::areNotBlockedBy(auth()->id())->where('verified', 1)->get();

3) Чтобы включить пользователей, которые заблокировали указанного пользователя ($id):

/**
 * Scope a query to only include users that blocked the specified user.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 * @param $id
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeWhoBlocked($query, $id)
{
    // How to do this? :)
}

Пример использования: User::whoBlocked(auth()->id())->where('verified', 1)->get();

4) Чтобы включить пользователей, которые не заблокировали указанного пользователя ($id):

/**
 * Scope a query to only include users that did not block the specified user.
 *
 * @param \Illuminate\Database\Eloquent\Builder $query
 * @param $id
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function scopeWhoDidNotBlock($query, $id)
{
    // How to do this? :)
}

Пример использования: User::whoDidNotBlock(auth()->id())->where('verified', 1)->get();


Как бы вы это сделали? Я ничего не нашел в Laravel документах по этому поводу (возможно, я пропустил это). (Я использую Laravel 6.x )

Я не уверен, но думаю, что это можно сделать двумя способами: Используя Left Join или используя необработанные запросы в , где в ... Я могу ошибаться, но я думаю, что решение с "левым соединением" будет лучше с точки зрения производительности, верно? (не уверен в этом, может быть, я совершенно не прав).

Ответы [ 2 ]

1 голос
/ 28 марта 2020

Вы можете использовать Запрос наличия отношений whereHas и Запрос отсутствия отношений whereDoesntHave функций построителя запросов для создания ваших результатов запросов.

Я включил каждый сгенерированный запрос SQL код и время запроса в миллисекундах, протестированные на выделенном сервере с двумя серверами Xeon в таблице с 1000 пользователей.

Мы не хотим получать текущего пользователя в результатах при запросе с помощью areNotBlockedBy и whoDidNotBlock, поэтому эти функции исключат пользователя с помощью $id.

  1. Чтобы включить пользователей, которые заблокированы указанным пользователем ($id) :

    /**
     * Scope a query to only include users that are blocked by the specified user.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeAreBlockedBy($query, $id)
    {
        return User::whereHas('blockedByUsers', function($q) use($id) {
            $q->where('user_id', $id);
        });
    }
    

    Выполнение:

    User::areBlockedBy(auth()->id())->where('verified', 1)->get();
    

    Сгенерирует следующее SQL:

    -- Showing rows 0 - 3 (4 total, Query took 0.0006 seconds.)
    select * from `users` where exists (select * from `users` as `laravel_reserved_9` inner join `ignore_lists` on `laravel_reserved_9`.`id` = `ignore_lists`.`user_id` where `users`.`id` = `ignore_lists`.`blocked_user_id` and `user_id` = ?) and `verified` = ?
    
  2. Включение пользователей, которые не заблокировано указанным пользователем ($id):

    /**
     * Scope a query to only include users that are not blocked by the specified user.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeAreNotBlockedBy($query, $id)
    {
        // It will exclude the user with $id
        return User::where('id', '!=', $id)
            ->whereDoesntHave('blockedByUsers', function($q) use($id) {
                $q->where('user_id', $id);
            });
    }
    

    Выполнение:

    User::areNotBlockedBy(auth()->id())->where('verified', 1)->get();
    

    Сгенерирует следующее SQL:

    -- Showing rows 0 - 24 (990 total, Query took 0.0005 seconds.)
    select * from `users` where `id` != ? and not exists (select * from `users` as `laravel_reserved_0` inner join `ignore_lists` on `laravel_reserved_0`.`id` = `ignore_lists`.`user_id` where `users`.`id` = `ignore_lists`.`blocked_user_id` and `user_id` = ?) and `verified` = ?
    
  3. Чтобы включить пользователей, которые заблокировали указанного пользователя ($id):

    /**
     * Scope a query to only include users that blocked the specified user.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeWhoBlocked($query, $id)
    {
        return User::whereHas('blockedUsers', function($q) use($id) {
            $q->where('blocked_user_id', $id);
        });
    }
    

    E xecuting:

    User::whoBlocked(auth()->id())->where('verified', 1)->get();
    

    Сгенерирует следующее SQL:

    -- Showing rows 0 - 1 (2 total, Query took 0.0004 seconds.)
    select * from `users` where exists (select * from `users` as `laravel_reserved_12` inner join `ignore_lists` on `laravel_reserved_12`.`id` = `ignore_lists`.`blocked_user_id` where `users`.`id` = `ignore_lists`.`user_id` and `blocked_user_id` = ?) and `verified` = ?
    
  4. Включение пользователей, которые не заблокировали указанного пользователя ($id):

    /**
     * Scope a query to only include users that did not block the specified user.
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param $id
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeWhoDidNotBlock($query, $id)
    {
        // It will exclude the user with $id
        return User::where('id', '!=', $id)
            ->whereDoesntHave('blockedUsers', function($q) use($id) {
                $q->where('blocked_user_id', $id);
            });
    }
    

    Выполнение:

    User::whoDidNotBlock(auth()->id())->where('verified', 1)->get();
    

    Будет сгенерировано следующее SQL:

    -- Showing rows 0 - 24 (992 total, Query took 0.0004 seconds.)
    select * from `users` where `id` != ? and not exists (select * from `users` as `laravel_reserved_1` inner join `ignore_lists` on `laravel_reserved_1`.`id` = `ignore_lists`.`blocked_user_id` where `users`.`id` = `ignore_lists`.`user_id` and `blocked_user_id` = ?) and `verified` = ?
    
1 голос
/ 28 марта 2020

Используйте join(inner join) производительность лучше, чем whereIn подзапрос.

В MySQL подвыборы в предложении IN повторно выполняются для каждой строки во внешнем запросе, в результате чего создается O(n^2).

Я думаю, что * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 101. * * * * * * * *. указанным user ($id), вы можете использовать этот метод напрямую:

User::where('id', $id)->first()->blockedUsers();

Подумайте сначала о применении where('verified', 1), поэтому вы можете использовать запрос как User::where('verified', 1)->areBlockedBy(auth()->id()), область действия может быть такой:

public function scopeAreBlockedBy($query, $id)
{
    return $query->whereHas('blockedByUsers', function($users) use($id) {
               $users->where('ignore_lists.user_id', $id);
           });
}

// better performance: however, when you apply another where condition, you need to specify the table name ->where('users.verified', 1)
public function scopeAreBlockedBy($query, $id)
{
    return $query->join('ignore_lists', function($q) use ($id) {
               $q->on('ignore_lists.blocked_user_id', '=', 'users.id')
                 ->where('ignore_lists.user_id', $id);
           })->select('users.*')->distinct();
}

Мы используем join для второго запроса, который повысит производительность, поскольку ему не нужно использовать where exists.

Пример для 300 000 записей в таблице пользователей:

Объясните первый запрос whereHas, который сканирует 301119+1+1 строк и занимает 575ms: whereHas explain whereHas times

Объяснить второй запрос join, который сканирует 3+1 строк и занимает 10.1ms: Join explain Join time

2) Включить пользователей, которые не заблокированы указанным user ($id), вы можете использовать whereDoesntHave закрытие, как этот:

public function scopeNotBlockedUsers($query, $id)
{
    return $query->whereDoesntHave('blockedByUsers', function($users) use ($id){
           $users->where('ignore_lists.user_id', $id);
     });
}

Я предпочитаю использовать whereDoesntHave вместо leftJoin здесь. Потому что, когда вы используете leftjoin, как показано ниже:

User::leftjoin('ignore_lists', function($q) use ($id) {                                                            
     $q->on('ignore_lists.blocked_user_id', '=', 'users.id') 
       ->where('ignore_lists.user_id', $id);
})->whereNull('ignore_lists.id')->select('users.*')->distinct()->get();

Mysql необходимо создать временную таблицу для хранения всех записей пользователей и объединить некоторые ignore_lists. И затем отсканируйте эти записи и найдите записи, которые без ignore_lists. whereDosentHave будет сканировать всех пользователей тоже. Для моего mysql сервера where not exists немного быстрее, чем left join. План его выполнения кажется хорошим. Производительность этих двух запросов не сильно отличается. whereDoesntHave explain left join is null

Для whereDoesntHave более читабельно. Я выберу whereDoesntHave. whereDoesntHave and leftjoin

3) Чтобы включить пользователей, которые заблокировали указанный user ($id), использовать whereHas заблокированных пользователей, например:

public function scopeWhoBlocked($query, $id)
{
    return $query->whereHas('blockedUsers', function($q) use ($id) {
                $q->where('ignore_lists.blocked_user_id', $id);
           });
}

// better performance: however, when you apply another where condition, you need to specify the table name ->where('users.verified', 1)
public function scopeWhoBlocked($query, $id)
{
    return $query->join('ignore_lists', function($q) use ($id) {
               $q->on('ignore_lists.user_id', '=', 'users.id')
                 ->where('ignore_lists.blocked_user_id', $id);
           })->select('users.*')->distinct();
}

4) Чтобы включить пользователей, которые не блокировали указанное user ($id), используйте whereDoesntHave для заблокированного пользователя:

public function scopeWhoDidNotBlock($query, $id)
{
    return $query->whereDoesntHave('blockedUsers', function($q) use ($id) {
                $q->where('ignore_lists.blocked_user_id', $id);
           });
}

PS: Не забудьте добавить индекс для foreign_key для ignore_lists Таблица.

...