Laravel Eloquent Внутреннее Объединение на Самообращающейся Таблице - PullRequest
2 голосов
/ 12 марта 2019

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

A пользователи таблица имеет отношение многие ко многим через pivot таблицу друзья

Я попытался и потерпел неудачу при внутреннем присоединении Users::class к себе. Лучшее, что я могу получить при внутреннем соединении, - это запустить два запроса и посмотреть, есть ли перекрытие. Таким образом, один человек обратился к другому, и наоборот.

friends   | users
----------|------
send_id   | id
receive_id| name
is_blocked|

выборка данных и ожидаемый результат

users.id | name
---------|------
1        | foo
2        | bar
3        | baz

friends
send_id | receive_id | is_blocked
--------|------------|-----------
1       |    2       |  0
2       |    1       |  0
1       |    3       |  0
3       |    1       |  1
2       |    3       |  0

У пользователя должны быть красноречивые отношения, называемые друзьями. Это должно быть то, что вы ожидаете получить от requestedFriends или receivedFriends только что присоединившихся.

foo->friends
returns `baz`
bar->friends
returns `foo`
baz->friends
returns empty collection

в настоящее время используется

// User.php
public function requestedFriends()
{
    $left = $this->belongsToMany(User::class, 'friends','send_id','receive_id')
        ->withPivot('is_blocked')
        ->wherePivot('is_blocked','=', 0)
        ->withTimestamps();
    return $left;
}

public function receivedFriends()
{
    $right = $this->belongsToMany(User::class, 'friends','receive_id','send_id')
        ->withPivot('is_blocked')
        ->wherePivot('is_blocked','=', 0)
        ->withTimestamps();

    return $right;
}

public function friends()
{
    $reqFriends = $this->requestedFriends()->get();
    $recFriends = $this->receivedFriends()->get();
    $req = explode(",",$recFriends->implode('id', ', '));
    $intersect = $reqFriends->whereIn('id', $req);
    return $intersect;
}

Исследования пока

Laravel Само-ссылающаяся таблица «многие ко многим» работает только в одну сторону -> старый вопрос, но все еще актуален

https://github.com/laravel/framework/issues/441#issuecomment-14213883 -> Да, это работает ... но в одну сторону.

https://laravel.com/docs/5.8/collections#method-wherein в настоящее время я нашел единственный способ сделать это красноречиво.

https://laravel.com/docs/5.7/queries#joins -> В идеале Я бы нашел решение, используя внутреннее соединение, но независимо от того, каким образом я поместил идентификаторы, я не мог заставить решение работать.

Решение будет

Решение будет внутренним присоединением к самореференсной таблице с использованием eloquent в laravel 5.7 или 5.8 , где связь существует, только если send_id & receive_id присутствует в нескольких строках в таблице друзей.

OR

Каким-то образом сообщите сообществу, что это невозможно сделать.

Заранее спасибо!

Ответы [ 2 ]

0 голосов
/ 18 апреля 2019

Я столкнулся с той же проблемой довольно давно, и поэтому внимательно следил за этой проблемой и провел много исследований.Я сталкивался с некоторыми решениями, которые вы также нашли, и с некоторыми другими, а также думал о других решениях, которые я суммировал здесь , в основном о том, как получить оба user_ids в одном столбце.Боюсь, что все они не будут работать хорошо.Я также боюсь, что использование любых пользовательских классов помешает вам использовать все удобные функции отношений Laravel (особенно нетерпеливую загрузку).Поэтому я все еще думал, что можно сделать, и, пока не появится функция hasMany для многих столбцов, я думаю, что вчера я нашел возможное решение.Сначала я покажу его, а затем применю к вашему проекту.

Мой проект

Первоначальное решение

В моем проекте один пользователь сотрудничает с другим (= партнерство) ипотом будет назначена комиссия.Итак, у меня были следующие таблицы:

USERS
id       | name
---------|------
1        | foo
2        | bar
17       | baz
20       | Joe
48       | Jane
51       | Jim 

PARTNERSHIPS
id  | partner1  | partner2  | confirmed | other_columns
----|-----------|-----------|-----------|---------------
1   | 1         | 2         | 1         |
9   | 17        | 20        | 1         |
23  | 48        | 51        | 1         |

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

//user.php
public function partnerships()
{
    $r = $this->hasMany(Partnership::class, 'partner1');

    if(! $r->count() ){
        $r = $this->hasMany(Partnership::class, 'partner2');
    }

    return $r;
}

Но если бы я хотел найти все партнерские отношения пользователя, текущего и прошлого, это, конечно, не сработало бы.

Новое решение

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

USERS
(same as above)

PARTNERSHIP_USER
user_id | partnership_id 
--------|----------------
1       | 1
2       | 1
17      | 9
20      | 9
48      | 23
51      | 23

PARTNERSHIPS
id  | confirmed | other_columns
----|-----------|---------------
1   | 1         |
9   | 1         |
23  | 1         |

// user.php
public function partnerships(){
    return $this->belongsToMany(Partnership::class);
}

public function getPartners(){
    return $this->partnerships()->with(['users' => function ($query){
        $query->where('user_id', '<>', $this->id);
    }])->get();
}

public function getCurrentPartner(){
    return $this->partnerships()->latest()->with(['users' => function ($query){
       $query->where('user_id', '<>', $this->id);
    }])->get();
}


// partnership.php
public function users(){
    return $this->belongsToMany(User::class);
}

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

Применительно к вашему проекту

В вашем примере таблицы могут быть структурированы следующим образом:

USERS
id       | name
---------|------
1        | foo
2        | bar
3        | baz

FRIENDSHIP_USER
user_id  | friendship_id
---------|------
1        | 1
2        | 1
3        | 2
1        | 2

FRIENDSHIPS 
id      |send_id* | receive_id* | is_blocked | [all the other nice stuff
--------|---------|-------------|------------|- you want to save]
1       | 1       |    2        |  0         |
2       | 3       |    1        |  0         |

[*send_id and receive_id are optional except 
you really want to save who did what]

Редактировать: My $user->partners() выглядит так:

// user.php

// PARTNERSHIPS
public function partnerships(){
    // 'failed' is a custom fields in the pivot table, like the 'is_blocked' in your example
    return $this->belongsToMany(Partnership::class)
        ->withPivot('failed');
}

// PARTNERS
public function partners(){
    // this query goes forth to partnerships and then back to users.
    // The subquery excludes the id of the querying user when going back
    // (when I ask for "partners", I want only the second person to be returned)
    return $this->partnerships()
        ->with(['users' => function ($query){
                $query->where('user_id', '<>', $this->id);
        }]);
}
0 голосов
/ 13 марта 2019

Я еще не проверял это решение во всех деталях, но я написал класс «ManyToMany», расширяющий класс «BelongsToMany», поставляемый с laravel, который, кажется, работает. Класс в основном просто переопределяет метод «get», дублируя исходный запрос, «инвертируя» его и просто выполняя «union» для исходного запроса.

<?php

namespace App\Database\Eloquent\Relations;

use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class ManyToMany extends BelongsToMany
{

    /**
     * Execute the query as a "select" statement.
     *
     * @param  array  $columns
     * @return \Illuminate\Database\Eloquent\Collection
     */
    public function get($columns = ['*'])
    {
        // duplicated from "BelongsToMany"
        $builder = $this->query->applyScopes();

        $columns = $builder->getQuery()->columns ? [] : $columns;

        // Adjustments for "Many to Many on self": do not get the resulting models here directly, but rather
        // just set the columns to select and do some adjustments to also select the "inverse" records
        $builder->addSelect(
            $this->shouldSelect($columns)
        );

        // backup order directives
        $orders = $builder->getQuery()->orders;
        $builder->getQuery()->orders = [];

        // clone the original query
        $query2 = clone($this->query);

        // determine the columns to select - same as in original query, but with inverted pivot key names
        $query2->select(
            $this->shouldSelectInverse( $columns )
        );
        // remove the inner join and build a new one, this time using the "foreign" pivot key
        $query2->getQuery()->joins = array();

        $baseTable = $this->related->getTable();
        $key = $baseTable.'.'.$this->relatedKey;
        $query2->join($this->table, $key, '=', $this->getQualifiedForeignPivotKeyName());

        // go through all where conditions and "invert" the one relevant for the inner join
        foreach( $query2->getQuery()->wheres as &$where ) {
            if(
                $where['type'] == 'Basic'
                && $where['column'] == $this->getQualifiedForeignPivotKeyName()
                && $where['operator'] == '='
                && $where['value'] == $this->parent->{$this->parentKey}
            ) {
                $where['column'] = $this->getQualifiedRelatedPivotKeyName();
                break;
            }
        }

        // add the duplicated and modified and adjusted query to the original query with union
        $builder->getQuery()->union($query2);

        // reapply orderings so that they are used for the "union" rather than just the individual queries
        foreach($orders as $ord)
            $builder->getQuery()->orderBy($ord['column'], $ord['direction']);

        // back to "normal" - get the models
        $models = $builder->getModels();
        $this->hydratePivotRelation($models);

        // If we actually found models we will also eager load any relationships that
        // have been specified as needing to be eager loaded. This will solve the
        // n + 1 query problem for the developer and also increase performance.
        if (count($models) > 0) {
            $models = $builder->eagerLoadRelations($models);
        }

        return $this->related->newCollection($models);
    }


    /**
     * Get the select columns for the relation query.
     *
     * @param  array  $columns
     * @return array
     */
    protected function shouldSelectInverse(array $columns = ['*'])
    {
        if ($columns == ['*']) {
            $columns = [$this->related->getTable().'.*'];
        }

        return array_merge($columns, $this->aliasedPivotColumnsInverse());
    }

    /**
     * Get the pivot columns for the relation.
     *
     * "pivot_" is prefixed ot each column for easy removal later.
     *
     * @return array
     */
    protected function aliasedPivotColumnsInverse()
    {
        $collection = collect( $this->pivotColumns )->map(function ($column) {
            return $this->table.'.'.$column.' as pivot_'.$column;
        });
        $collection->prepend(
            $this->table.'.'.$this->relatedPivotKey.' as pivot_'.$this->foreignPivotKey
        );
        $collection->prepend(
            $this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->relatedPivotKey
        );

        return $collection->unique()->all();
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...