Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
907 views
in Technique[技术] by (71.8m points)

php - Laravel Eloquent Inner Join on Self Referencing Table

I'm trying to inner join a users table to itself using an eloquent model. I've looked everywhere but can't seem to find a solution to this without creating two queries which is what I am currently doing.

A users table has a many to many relationship itself through the pivot table friends

I tried and failed inner joining Users::class to itself. The best I can get at an inner join is by running two queries and seeing if there is an overlap. Thus one person has reached out to the other and vice versa.

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

sample data & expected result

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

The user should have an eloquent relationship called friends. It should be what you expect comes out of requestedFriends or receivedFriends just joined.

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

currently using

// 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;
}

Research so far

Laravel Many to many self referencing table only works one way -> old question, but still relevant

https://github.com/laravel/framework/issues/441#issuecomment-14213883 -> yep, it works… but one way.

https://laravel.com/docs/5.8/collections#method-wherein currently the only way I have found to do this in eloquent.

https://laravel.com/docs/5.7/queries#joins -> Ideally I would find a solution using an innerjoin onto itself, but no matter which way I put the id's I couldn't get a solution to work.

A solution would

A solution would inner join a self referencing table using eloquent in laravel 5.7 or 5.8, where a relationship only exists if send_id & receive_id are present on multiple rows in the friends table.

OR

Somehow let the community know that this can't be done.

Thanks in advance!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

I have not checked this solution in every detail yet, but I have written a "ManyToMany" Class extending the "BelongsToMany" Class shipped with laravel, which appears to work. The class basically just overrides the "get" method, duplicating the original query, "inverting" it and just performing a "union" on the original query.

<?php

namespace AppDatabaseEloquentRelations;

use IlluminateDatabaseEloquentRelationsBelongsToMany;

class ManyToMany extends BelongsToMany
{

    /**
     * Execute the query as a "select" statement.
     *
     * @param  array  $columns
     * @return IlluminateDatabaseEloquentCollection
     */
    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();
    }

}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...