简体   繁体   中英

Query multiple, nested Relationships in Laravel/Eloquent

I'm trying to build a simple news feed for posts in Laravel with Eloquent.

I basically want to retrieve all Posts ...

  • where I am author
  • where people I follow are author (followable)
  • where people I follow have commented on
  • where people with same field_id are author
  • where poeple with same school_id are author

in one query.

As I never worked intensivly with joined/combined SQL queries, any help on this is greatly appreciated!

My Tables

users table

+----+
| id |
+----+

posts table

+----+-----------+-------------+
| id | author_id | author_type |
|----|-----------|-------------|
|    | users.id  | 'App\User'  |
+----+-----------+-------------+

comments table

+----+----------------+------------------+-----------+-------------+
| id | commentable_id | commentable_type | author_id | author_type |
|----|----------------|------------------|-----------|-------------|
|    | posts.id       | 'App\Post'       | users.id  | 'App\User'  |
+----+----------------+------------------+-----------+-------------+

schoolables table

+---------+-----------+----------+
| user_id | school_id | field_id |
+---------+-----------+----------+

followables table

+-------------+---------------+---------------+-----------------+
| follower_id | follower_type | followable_id | followable_type |
|-------------|---------------|---------------|-----------------|
| users.id    | 'App\User'    | users.id      | 'App\User'      |
+-------------+---------------+---------------+-----------------+

My Models

class Post extends Model
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function author()
    {
        return $this->morphTo();
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Comment extends Model
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function author()
    {
        return $this->morphTo();
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class User extends Model
{
    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
     */
    public function posts()
    {
        return $this->morphMany(Post::class, 'author')->orderBy('created_at', 'desc');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
     */
    public function comments()
    {
        return $this->morphMany(Comment::class, 'author')->orderBy('created_at', 'desc');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function schoolables()
    {
        return $this->hasMany(Schoolable::class);
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
     */
    public function following()
    {
        return $this->morphMany(Followable::class, 'follower');
    }
}

You can try to use left joins for this query, but it would become a complex thing, because you have to make all the joins with leftJoins and then a nested orWhere clause on all the

$posts = Post::leftJoin('..', '..', '=', '..')
  ->where(function($query){
    $query->where('author_id', Auth::user()->id); // being the author
  })->orWhere(function($query){
    // second clause...
  })->orWhere(function($query){
    // third clause...
  .....
  })->get();

I don't think this will be manageable, so I would advice using UNIONS, http://laravel.com/docs/5.1/queries#unions

So it would be something like..

$written = Auth::user()->posts();
$following = Auth::user()->following()->posts();

After getting the different queries, without getting the results, you can unite them..

$posts = $written->union($following)->get();

Hopefully this will direct you in the right direction

Okay, so you want to retrieve all posts where any of the 5 conditions above apply. The trick to writing such queries is to break them up into smaller, more manageable pieces.

$query = Post::query();

So let's say you are $me .

The ids of users you are following can be obtained with

$followingUserIds = $me
    ->following()
    ->where('followable_type', User::class)
    ->lists('followable_id');

The ids of users in the same fields as you can be obtained with

$myFieldIds = $me->schoolables()->lists('field_id');
$sharedFieldUserIds = Schoolable::whereIn('field_id', $myFieldIds)->lists('user_id');

Similarly, users in the same school as you can be obtained with

$mySchoolIds = $me->schoolables()->lists('school_id');
$sharedSchoolUserIds = Schoolable::whereIn('school_id', $mySchoolIds)->lists('user_id');

Let's define each of those conditions:

Where I am author

$query->where(function($inner) use ($me) {
    $inner->where('posts.author_type', User::class);
    $inner->where('posts.author_id', $me->id);
});

Where people I follow are author (followable)

$query->orWhere(function($inner) use ($followingUserIds) {
    $inner->where('posts.author_type', User::class);
    $inner->whereIn('posts.author_id', $followingUserIds);
});

where people I follow have commented on
This one is actually slightly tricky: we need to use the ->whereHas construct, which finds posts with at least 1 comment matching the subquery.

$query->orWhereHas('comments', function($subquery) use ($followingUserIds) {
    $subquery->where('comments.author_type', User::class);
    $subquery->whereIn('comments.author_id', $followingUserIds);
});

The remaining two are simple.

where people with same field_id are author

$query->orWhere(function($inner) use ($sharedFieldUserIds) {
    $inner->where('posts.author_type', User::class);
    $inner->whereIn('posts.author_id', $sharedFieldUserIds);
});

and you can see where this is going

where poeple with same school_id are author

$query->orWhere(function($inner) use ($sharedSchoolUserIds) {
    $inner->where('posts.author_type', User::class);
    $inner->whereIn('posts.author_id', $sharedSchoolUserIds);
});

To get the matching posts, you just need to do

$posts = $query->get();

While constructing the query from scratch works in this particular case, it will create a fairly brittle structure if your requirements ever change. For added flexibility, you probably want to build query scopes on the Post and Comments models for each of those components. That will mean that you only need to figure out one time what it means for a post to be authored by someone a user follows, then you could simply do Post::authoredBySomeoneFollowedByUser($me) to get the collection of posts authored by someone you follow.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM