36

I'm using Laravel and having a small problem with Eloquent ORM.. I can get this working simply with SQL query using a JOIN but I can't seem to get it working with Eloquent!

This is what I want, I have two tabels. one is 'Restaurants' and other is 'Restaurant_Facilities'.

The tables are simple.. and One-To-One relations. like there is a restaurant table with id, name, slug, etc and another table called restaurant_facilities with id, restaurant_id, wifi, parking, etc

Now what I want to do is.. load all restaurants which have wifi = 1 or wifi = 0.. How can i do that with Eloquent ? I have tried eager loading, pivot tables, with(), collections() and nothing seems to work!

The same problem I have for a Many-To-Many relation for cuisines! I have the same restaurant table and a cuisine table and a restaurant_cuisine_connection table..

but how do I load all restaurants inside a specific cuisine using it's ID ?

This works.

Cuisine::find(6)->restaurants()->get();

but I wanna load this from Restaurant:: model not from cuisines.. because I have many conditions chained together.. its for a search and filtering / browse page.

Any ideas or ways ? I've been struggling with this for 3 days and still no answer.

Example Models :

class Restaurant extends Eloquent {

    protected $table = 'restaurants';

    public function facilities() {
        return $this->hasOne('Facilities'); 
    }
}

class Facilities extends Eloquent {

    protected $table = 'restaurants_facilities';

    public function restaurant() {
        return $this->belongsTo('Restaurant');
    }

}

PS : This seems to be working.. but this is not Eloquent way right ?

Restaurant::leftJoin(
                'cuisine_restaurant', 
                'cuisine_restaurant.restaurant_id', 
                '=', 'restaurants.id'
             )
             ->where('cuisine_id', 16)
               ->get();

Also what is the best method to find a count of restaurants which have specific column value without another query ? like.. i have to find the total of restaurants which have parking = 1 and wifi = 1 ?

Please help on this.

Thank you.

Jeff Puckett
  • 37,464
  • 17
  • 118
  • 167
Tharshan Venkdesan
  • 363
  • 1
  • 3
  • 7
  • You should accept the answer that was most helpful to you :) – Alex Naspo Feb 02 '13 at 18:19
  • 1
    There's nothing wrong with using a join. Laravel uses sub-queries for constraints on eager loads, and if you have a lot of data, that is going to be less efficient than a join. Laravel's approach to filtering is often a 20lb sledgehammer when what you really need is a scalpel. – Dave Nov 13 '15 at 18:12

5 Answers5

34

I don't see anything wrong with doing the left join here, if you have to load from the Restaurant model. I might abstract it away to a method on my Restaurant model, like so:

class Restaurant extends Eloquent {
    protected $table = 'restaurants'; // will be default in latest L4 beta

    public function facility()
    {
      return $this->hasOne('Facility');
    }

    // Or, better, make public, and inject instance to controller.
    public static function withWifi()
    {
      return static::leftJoin(
        'restaurant_facilities',
        'restaurants.id', '=', 'restaurant_facilities.restaurant_id'
      )->where('wifi', '=', 1);
    }
}

And then, from your routes:

Route::get('/', function()
{
  return Restaurant::withWifi()->get();
});

On the go - haven't tested that code, but I think it should work. You could instead use eager loading with a constraint, but that will only specify whether the facility object is null or not. It would still return all restaurants, unless you specify a where clause.

(P.S. I'd stick with the singular form of Facility. Notice how hasOne('Facilities') doesn't read correctly?)

JeffreyWay
  • 1,143
  • 9
  • 11
  • Thanks for the Idea. Yeah I realise that using Join is not wrong. but I was confused because even with eager loading I can't use where conditions for eager loaded relationship's objects. Thanks for the answer. :) – Tharshan Venkdesan Feb 01 '13 at 06:46
20

I stumbled across this post while trying to improve my REST API methodology when building a new sharing paradigm. You want to use Eager Loading Constraints. Let's say you have an api route where your loading a shared item and it's collection of subitems such as this:

/api/shared/{share_id}/subitem/{subitem_id}

When hitting this route with a GET request, you want to load that specific subitem. Granted you could just load that model by that id, but what if we need to validate if the user has access to that shared item in the first place? One answer recommended loading the inversed relationship, but this could lead to a confusing and muddled controller very quickly. Using constraints on the eager load is a more 'eloquent' approach. So we'd load it like this:

$shared = Shared::where('id', $share_id)
  ->with([ 'subitems' => function($query) use ($subitem_id) {
    $query->where('subitem_id', $subitem_id)
  }]);

So where only want the subitem that has that id. Now we can check if it was found or not by doing something like this:

if ($shared->subitems->isEmpty())

Since subitems is a collection (array of subitems) we return the subitem[0] with this:

return $shared->subitems[0];
Throttlehead
  • 1,927
  • 6
  • 22
  • 36
  • You need to add semi-colon (`;`) after this line `$query->where('subitem_id', $subitem_id)` Other wise, We will catch syntax error. – Bhavin Thummar Jul 17 '19 at 12:51
15

Use whereHas to filter by any relationship. It won't join the relation but it will filter the current model by a related property. Also look into local scopes to help with situations like this https://laravel.com/docs/5.3/eloquent#local-scopes

Your example would be:

Restaurant::whereHas('facilities', function($query) {
    return $query->where('wifi', true);
})->get();


Restaurant::whereHas('cuisines', function($query) use ($cuisineId) {
    return $query->where('id', $cuisineId);
})->get();

To achieve the same thing with local scopes:

class Restaurant extends Eloquent
{
    // Relations here

    public function scopeHasWifi($query)
    {
        return $query->whereHas('facilities', function($query) {
            return $query->where('wifi', true);
        });
    }

    public function scopeHasCuisine($query, $cuisineId)
    {
        return $query->whereHas('cuisines', function($query) use ($cuisineId) {
            return $query->where('id', $cuisineId);
        });
    }
}

For local scopes you DO NOT want to define them as static methods on your model as this creates a new instance of the query builder and would prevent you from chaining the methods. Using a local scope will injects and returns the current instance of the query builder so you can chain as many scopes as you want like:

Restaurant::hasWifi()->hasCuisine(6)->get();

Local Scopes are defined with the prefix scope in the method name and called without scope in the method name as in the example abover.

Eric Tucker
  • 6,144
  • 1
  • 22
  • 36
10

Another solution starring whereHas() function:

$with_wifi = function ($query) {
   $query->where('wifi', 1);
};

Facilities::whereHas('restaurant', $with_wifi)

Nice and tidy.

Yauheni Prakopchyk
  • 10,202
  • 4
  • 33
  • 37
1

Do you absolutely have to load it from the Restaurant model? In order to solve the problem, I usually approach it inversely.

Facilities::with('restaurant')->where('wifi' ,'=', 0)->get();

This will get all the restaurant facilities that match your conditions, and eager load the restaurant.

You can chain more conditions and count the total like this..

Facilities::with('restaurant')
  ->where('wifi' ,'=', 1)
  ->where('parking','=', 1)
  ->count();

This will work with cuisine as well

Cuisine::with('restaurant')->where('id','=',1)->get();

This grabs the cuisine object with the id of 1 eager loaded with all the restaurants that have this cuisine

Alex Naspo
  • 2,052
  • 1
  • 20
  • 37
  • Thanks for the answer. Yeah I used this method but its a very big chained query with filters so I prefer to use Restaurants model always. EDITED : Didn't read your post clearly!! Thanks :) Its useful – Tharshan Venkdesan Feb 01 '13 at 06:44
  • That doesn't work for me. The `with` can not have a `where` like you are pointing out. It should be done with a callback like detailed in **Eager Load Constraints** [at the documentation](http://laravel.com/docs/eloquent#eager-loading) – Alvaro Jan 30 '14 at 15:35