Laravel Many to many - Unexpected result set on ->

2019-06-06 07:49发布

问题:

I wonder if anyone can help, as I've hit a wall and still learning Laravel ORM. Can anyone explain why, when I run:

public function locationTags(){
    return $this->hasMany('App\UserHasLocationTags', 'user_id')
        ->join('location_tags AS lt', 'lt.id', '=', 'location_tag_id');
}

I get this result set: (snipped...)

{
    "id": 1,
    "created_at": "2015-05-13 13:04:56",
    "updated_at": "2015-05-13 13:04:56",
    "email": "REMOVED",
    "firstname": "REMOVED",
    "lastname": "REMOVED",
    "location_id": 0,
    "deleted_at": null,
    "permissions": [],
    "location_tags": [
        {
            "user_id": 1,
            "location_tag_id": 1,
            "id": 1,
            "created_at": "2015-05-13 13:06:28",
            "updated_at": "2015-05-13 13:06:28",
            "name": "Test Tag 0",
            "location_id": 1,
            "deleted_at": null
        },
        {
            "user_id": 1,
            "location_tag_id": 2,
            "id": 2,
            "created_at": "2015-05-13 11:40:21",
            "updated_at": "2015-05-13 12:56:13",
            "name": "Test Tag 123",
            "location_id": 1,
            "deleted_at": null
        }
    ]
}

Which is ace! However, when I start to select the columns I want from the location_tags join, with:

public function locationTags(){
    return $this->hasMany('App\UserHasLocationTags', 'user_id')
        ->join('location_tags AS lt', 'lt.id', '=', 'location_tag_id')
        ->select('lt.id', 'lt.name');
}

I end up with:

{
    "id": 1,
    "created_at": "2015-05-13 13:04:56",
    "updated_at": "2015-05-13 13:04:56",
    "email": "REMOVED",
    "firstname": "REMOVED",
    "lastname": "REMOVED",
    "location_id": 0,
    "deleted_at": null,
    "permissions": [],
    "location_tags": []
}

Can someone explain what's going on? And possibly point me in the right direction to limit the selects? Thanks!

Update I've also tried:

        $query = \App\User::with(['permissions', 'locationTags' => function($query){
            $query->select('lt.id', 'lt.name');
        }]);

Which returns the same result :(

回答1:

Figured it out. The key here was that you must include a select() value of at least one key that Laravel can use to map the result set. In my case it was user_id, like so:

public function locationTags(){
    return $this->hasMany('App\UserHasLocationTags', 'user_id')
        ->join('location_tags AS lt', 'lt.id', '=', 'location_tag_id')
        ->select('user_id', 'lt.name', 'location_tag_id');
}

Which then returns a much nicer results set:

{
    "id": 1,
    "created_at": "2015-05-13 13:04:56",
    "updated_at": "2015-05-13 13:04:56",
    "email": "REMOVED",
    "firstname": "REMOVED",
    "lastname": "REMOVED",
    "location_id": 0,
    "deleted_at": null,
    "permissions": [],
    "location_tags": [
        {
            "user_id": 1,
            "name": "Test Tag 0",
            "location_tag_id": 1
        },
        {
            "user_id": 1,
            "name": "Test Tag 123",
            "location_tag_id": 2
        }
    ]
}

Hope this helps someone out in the future, because it kept me guessing for a good couple of hours.



回答2:

I'm sorry but you've gone a little sideways here. The relationship definition should just define the Relationship. It's a powerful feature that supports many other aspects of the ORM. What you've done here is more or less build a custom Query, severely limiting the effectiveness of the relationship.

Up until recently, the proper way would have looked something like this.

// class User
public function userHasLocationTags() {
    $this->hasMany('App\UserHasLocationTags', 'user_id');
}

// class UserHasLocationTags
public function locationTags() {
    $this->hasMany('App\LocationTags', 'location_tag_id');
}

And you would eager load all the results like this.

$user = User::where('id', 1)->with('userHasLocationTags.locationTags')->first();

The code above produces in 3 queries. One to get the User, one to get all the UserHasLocationTags and a one to get all the LocationTags. This may seem wasteful on first blush but consider the following.

$users = User::take(100)->with('userHasLocationTags.locationTags')->get();

Again, this is only 3 queries but now you have loaded 100 users with all their location tags.

But I can see you are a man with an eye for efficiency, and loading all the intermediate relationships and the whole nested hierarchy probably doesn't sit well with you. Well good news! Laravel 5 has added another relationship type for just this situation. hasManyThrough (scroll down a bit to find it).

Has Many Through

The "has many through" relation provides a convenient short-cut for accessing distant relations via an intermediate relation. For example, a Country model might have many Post through a User model.

So in your case it may look something like this...

// class User
public function locationTags()
{
    return $this->hasManyThrough('App\LocationTags', 'App\UserHasLocationTags');
}

$users = User::take(100)->with('locationTags')->get();

So now we are down to two queries. If you feel the need to optimize this further and only select specific columns, you can modify the eager load.

$users = User::take(100)->with(['locationTags' => function ($query)
{
    $query->select('user_id', 'name', 'location_tag_id');
})->get();

And if you do that frequently, you should wrap it up in a scope.