Laravel Eloquent自引用表的内部连接

4
我想要使用Eloquent模型将用户表自我关联并执行内连接,但无法找到不创建两个查询的解决方案,目前我正在使用这种方法。用户表通过中间表friends与自己建立多对多的关系。我曾尝试过使用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 目前我发现在eloquent中这是唯一的方法。

https://laravel.com/docs/5.7/queries#joins -> 理想情况下,我会找到一个使用内连接到自身的解决方案,但无论我怎样放置ID,都无法让解决方案起作用。

一个解决方案应该

一个解决方案应该在laravel 5.75.8中使用eloquent来内连接自引用表,只有在friends表中多个行中存在send_idreceive_id时才存在关系。

或者

以某种方式让社区知道这是做不到的。

谢谢您的帮助!


请提供一些样本数据和期望的结果。 - Jonas Staudenmeir
2个回答

2

我还没有仔细检查这个解决方案,但是我已经编写了一个“ManyToMany”类来扩展laravel中提供的“BelongsToMany”类,它似乎可以工作。

该类基本上只是覆盖了“get”方法,复制了原始查询,“反转”它并在原始查询上执行“union”操作。

"Original Answer"翻译成"最初的回答"

<?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();
    }

}

1

我很久以前遇到了同样的问题,因此一直在密切关注这个问题并进行了大量的研究。我发现了一些你也找到的解决方案,还有一些其他的解决方案,我在这里总结了大部分内容,主要是如何将两个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]

编辑:我的$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);
        }]);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接