Я пытаюсь внутреннее соединение таблицы пользователей с самой собой с использованием красноречивой модели. Я искал везде, но не могу найти решение этой без создания двух запросов, чем я сейчас и занимаюсь.
Таблица пользователи сама имеет связь многие ко многим через таблицу вращатьсядрузья.
Я попытался и не смог выполнить внутреннее соединение 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-где в настоящее время единственный способ, которым я нашел, сделать это красноречиво.
https://laravel.com/docs/5.7/queries#joins -> в идеале Я бы нашел решение, используя внутреннее соединение с самим собой, но независимо от того, каким образом я помещал идентификаторы, я не мог заставить решение работать.
Решением было бы внутреннее соединение таблицы с самоссылками с использованием красноречия в ларавель 5.7 или 5,8, где связь существует только в том случае, если send_id и receive_id присутствуют в нескольких строках в таблице друзей.
ИЛИ
Каким-то образом сообщите сообществу, что это невозможно.
Заранее спасибо!






Я еще не проверил это решение во всех деталях, но я написал класс «ManyToMany», расширяющий класс «BelongsToMany», поставляемый с laravel, который, похоже, работает. Класс в основном просто переопределяет метод «получить», дублируя исходный запрос, «инвертируя» его и просто выполняя «объединение» исходного запроса.
<?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();
}
}
Я столкнулся с той же проблемой довольно давно и поэтому внимательно следил за этой проблемой и провел много исследований. Я столкнулся с некоторыми решениями, которые вы также нашли, и некоторыми другими, а также подумал о других решениях, которые я суммировал здесь, в основном о том, как получить оба user_id в одном столбце. Я боюсь, что все они не будут работать хорошо. Я также боюсь, что использование любых пользовательских классов помешает вам использовать все удобные функции отношений 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]
Редактировать: Мой $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);
}]);
}
Предоставьте примерные данные и ожидаемый результат.