1

I have some relationships (that i can hopefully explain correctly) and need to sort the output by what is essentially a distant relation.

I have a pivot table that contains details for many of the relations, including that that i want to sort by.

--User.php

public function players()
{
    return $this->belongsToMany('App\Models\Player', 'league_player_user')->withPivot('position_id');
}

--Player.php

public function position()
{
    return $this->belongsToMany('App\Models\Position', 'league_player_user');
}

I will be eager loading the relationship like so;

$user = User::with('players')->where('id', Auth::user()->id)->first();

So, i think i want to do something like this (does not work).

$user = User::with(['players' => function($q){
    $q->orderBy('position.sort_id', 'asc');
}])->where('id', Auth::user()->id)->first();

The pivotal Table structure looks a little like this;

league_player_user.
...league_id
...player_id
...user_id
...position_id

The Positions table contains the sort_id

Hopefully this is enough information, please request more if needed. Many thanks.

Alex
  • 2,003
  • 4
  • 19
  • 30
  • The `belongsToMany` in your `User` model will join the pivot table automatically (and the far table (`players`)), but will not join the `position` table (even though you say you want the `position_id` column. As such, you need to ensure you add the extra join yourself. I think you can do this with `User::with('players', 'players.position')` (though as `position_id` is a virtual field from the pivot relationship, you may not be able to do it like this, you'll have to play with the code). The point is that, `position` is not automatically joined in, so you can't order on it. – alexrussell May 14 '15 at 09:03
  • Thanks @alexrussell. I have looked at this, yes - eager loading the nested relation also. And then I can user the orderBy() query in the players.position, and although it does not error, it does not sort the results correctly either.. – Alex May 14 '15 at 09:14
  • Instead of using `->first()` use `->toSql()` to return the SQL it's generating - this will help you debug exactly what the ORM is coming up with. – alexrussell May 14 '15 at 12:15
  • Thanks @alexrussell, but this is not gleaming any results for me in this case - it doesn't seem to offer up any details that will help me! – Alex May 15 '15 at 09:33
  • Can yo update the question with the output of `->toSql()` so we can see? Maybe the power of a group can see why the SQL is dodgy (if it's dodgy). – alexrussell May 15 '15 at 14:48
  • Thanks for your perseverance, @alexrussell! It's strange, the toSql() simply returns `select * from `users` where `id` = ?`... I am at a total loss here and not sure what else to try; maybe i need some kind of join in the query for with('players') ... then i can run the orderBy? – Alex May 15 '15 at 15:06
  • Ahh. I forgot that Laravel doesn't actually join the tables mentioned in `->with()` to the query, but actually does a secondary query to get those, and then manually associates them to the original models. As such, my original advice indeed won't work. However, I think you can use the standard Laravel DB stuff with Eloquent, so maybe try manual joins (not as pretty code, for sure) like `User::join('league_player_user', 'league_player_user.user_id', '=', 'users.id')->join('positions', 'league_player_user.postion_id', '=', 'position.id')->orderBy('position.sort_id', 'asc');` kinda thing? – alexrussell May 15 '15 at 15:53
  • Thanks @alexrussell - we could be getting somewhere now..! However; i am not an expert and i do not know what i should do with this data. For example, i was looping through the $user->players (foreach). But now i cannot do that with what is being returned, but there IS something being returned..! Thanks again – Alex May 15 '15 at 16:08
  • 1
    Okay I think I see what you're trying to do - get a single user but with their players already populated and ordered by position. Might I suggest that for the purposes of making your query more simple you don't eager load the players. As such, all you'll need to do upfront is `$user = Auth::user();`. Then, on the relationship (the `players()` method) you do the special ordering work: `return $this->belongsToMany('App\Models\Player', 'league_player_user')->withPivot('position_id')->join('position', 'league_player_user.position_id', '=', 'position.id')->orderBy('position.sort_id');` – alexrussell May 18 '15 at 08:32
  • Of course, you could also do that `->join('position', 'league_player_user.position_id', '=', 'position.id')->orderBy('position.sort_id');` in that `->withPlayers()` subquery if you did want to eager load the players. But it seems to me that whenever you get the players (be it in this query or some other `$user->players` call) you will want them ordered, so you should do the ordering on the relationship call itself. – alexrussell May 18 '15 at 08:33
  • @alexrussell - You sir, are a legend. That cracks it for me mate and i can use the same join either on the model itself of where i am making the query; both of which will give me the right results, and most importantly, in the **right order**. Thanks again! (if you compose an answer, i can mark it as correct) – Alex May 18 '15 at 13:42
  • We got there in the end :) Answer posted. – alexrussell May 18 '15 at 14:11

1 Answers1

1

Okay so you're trying to get a single user but with their players (where users bTM players) already populated and ordered by position (where pivot bT position).

In this case you will not be able to use Laravel's inbuilt relationship methods without modification, because Laravel just wasn't built to hand relationships that are on the pivot table of other relationships. Luckily, the ORM is flexible enough to allow you to do what you want with a 'manual' join.

So to answer your question directly, here's the kind of code you require (you were really close!):

$user = User::with(['players' => function ($q) {
    $q->join('position', 'league_player_user.position_id', '=', 'position.id')
      ->orderBy('position.sort_id', 'asc');
}])->where('id', Auth::user()->id)->first();

However, it appears to me that that's not great code for a few reasons:

  1. You're manually getting the authorised user (when Auth::user() is so convenient)
  2. You're actually having to take implementation-specific logic from the model (the fact that the pivot table is called league_player_user and put it... well wherever this is (your controller?)
  3. This will only affect this one single query - if you happened to get a User some other way (e.g. Auth::user() or User::find(1) or whatever) you won't have the players ordered correctly

As such, might I suggest that for the purposes of making your query more simple you don't eager load the players. As such, all you'll need to do upfront is:

$user = Auth::user();

Now, on the relationship (the players() method in your User class) you do the special ordering work:

public function players()
{
    return $this->belongsToMany('App\Models\Player', 'league_player_user')
                ->withPivot('position_id')
                ->join('position', 'league_player_user.position_id', '=', 'position.id')
                ->orderBy('position.sort_id');
}

This way, any time you call $user->players, you will get them ordered correctly.

I must just add that doing it this way may not allow you to eager load the players, as Laravel's eager loading (i.e. using ->with() in the ORM chain) due to the way that Laravel does the eager loading queries - one for main query (i.e. users) and one for the relationship (i.e. players) but it does this query to get all results, so won't work with out special ordering system possibly. You'll have to see if you really care about eager loading the players. In your case above (where you're getting the single authorised user), eager loading is not as important in my opinion.


Edit to add clarification regarding eager loading:

My reasoning behind suggesting that eager loading may not work is that eager loading in Laravel is done kinda like this: say you have categories and products: Category hM Product, Product bT Category. To get a category you use $category = Category::find(1) and Laravel turns that into a query a bit like this: SELECT * FROM `categories` WHERE `id` = '1'. If you then call $category->products Laravel will issue SELECT * FROM `products` WHERE `category_id` = '1'. That's sensible. But if you had the following code it'd be worse:

<?php $categories = Category::all(); ?>

<ul>
    @foreach ($categories as $category)
        <li>Category {{{ $category->name }}} contains {{{ $category->products->count() }}} products.</li>
    @endforeach
</ul>

In that case you'd have the following queries:

SELECT * FROM `categories`;
SELECT * FROM `products` WHERE `category_id` = '1';
SELECT * FROM `products` WHERE `category_id` = '2';
SELECT * FROM `products` WHERE `category_id` = '3';
... as many categories as you had

However, if you were to change that first line to this:

<?php $categories = Category::with('products')->get(); ?>

Now you'd only have two queries:

SELECT * FROM `categories`;
SELECT * FROM `products` WHERE `category_id` IN ('1', '2', '3', ...);

Laravel then, after calling the second query, will create the various collections for you based on the category IDs it knows you have.

However, that's the simplistic relationship case. In your case, the products() method is not just return $this->hasMany('Product'); but it includes a pivot and a manual join, etc., and it's possible that that second query, which is the eager loading one, just won't be able to cope and do that ordering properly.

As I said, I don't know for certain how this works, it's just a bit of a red flag for me. By all means give it a go and see what you get - you may find that it works for you.

alexrussell
  • 13,856
  • 5
  • 38
  • 49
  • This is totally the right answer. However you mention that eager loading may not be possible for in this way, without some modifications. In this particular query, it would result in many many more DB queries... Is this not something i should be worried about? (up to 30 for the one page..) – Alex May 18 '15 at 14:49
  • Well actually I think you're mixing up two things I said (but tat'd my fault - I wasn't very clear). I said you won't be able to do what you want without modifications. I then give you those modifications. Then at the end I suggest that doing this with eager loading may not work. I will add my reasoning to the bottom of the answer now as it's a bit of an in-depth explanation. But in the end I don't really *know* how Laravel does this eager loading as I have only used it in the more simplistic case, so if you want to use eager loading, try it and see if it works. – alexrussell May 18 '15 at 15:30