Introduction: deeper Eloquent relationships
Hello guys. Today I want to talk about Eloquent relationships and specifically about deeper relationships than one level. Generally we are working with hasMany, belongsTo, or many-to-many; these are the most popular types of relationships. But what if you have a relationship some deeper level? For example, countries table, then each country has some cities so city belongs to a country, and then in the city something else happens — level number three. For example, shops, and every shop belongs to a city. You may have tasks to query the data using first level, second level, and third level, and belongsTo isn't enough. Well, it is enough but the query gets more complex. How to deal with those three levels or even more levels deep with Eloquent functions like hasManyThrough relationship and a few packages? Let me show you an example.
Example: country shops count with hasManyThrough
What if you want to query the country list with the amount of shops per country? Remember there's country, then city, and then shop, so you cannot immediately query the relationship unless you specify so-called hasManyThrough relationship. If we go to Country database model, generally what you have is hasMany for the cities, but you can also define a relationship called shops with return this hasManyThrough and then related table is Shop with another related model which is City related to the Shop via city or through city. Then you can query the shops, and not only that you can query with count. For example, we will have Country::get and inside of that we'll have Country::with('shops') for example like this one. In app/Models/Country added here: country and shops. In the index blade I've made an old-school table, even without any HTML, even with older deprecated border one. So for each of the countries, shops count like this, and let's try it out. Let's launch that page and we have the results. This is zoomed-in version; I've seeded 50 random shops. So this is how you can query shops count with hasManyThrough relationship, or you can do it even with withCount of shops. Then you download all the shops models if you load only the number of the shops and you access that with shops_count as a property. If we refresh that the result should be the same — it's absolutely the same. Let's take a look at the database query, what is inside, what is launching under the hood. For that we will install a package called Debugbar: composer require barryvdh/laravel-debugbar. I'm typing that from memory and that's all we need to do to install it. We don't need to enable anything; we just refresh the page and we have queries.
Inspecting SQL with Debugbar
We have queries and this is the query. Actually let's zoom out a bit so we can actually read that. So select country and then there's a subquery for select count from shops joining cities and joining the countries, and this is one pretty fast Eloquent query.
N+1 problem and eager loading city → country
The second example is the other way around: what if you want to query the list of the shops and list the country for every shop? Generally, first I will show you the wrong way. Getting all the shops and then we have relationships. Relationships are described in model: Shop belongsTo a City and then City belongsTo a Country. Then in the controller and the blade file of index we do for each of the shops we show the name and we show shop->city->country->name. It is working, so the list is here. The problem however with that list is the amount of queries launched to the database. Every time it's launching the city, it is launching a query to the database, and also to the country — acquired to the database. If we see the Debugbar, select from city, select from countries are happening all the time for the 50 shops and there are 100 queries in total. To avoid that of course we can use eager loading: Shop::with('city.country')->get. Then if we refresh there are only three queries, so we get the shops, we select from cities with only the cities that we need, and select from countries with only the countries that we need. That is great — three queries. But another problem here is that we load all the cities and the countries, although we don't really need the cities actually.
To try to solve that problem we will use a package from Jonas Staudenmeyer; he released quite a few packages about relationships and we will try a few of them in this video. One of them is called BelongsToThrough, which is the opposite of hasManyThrough. hasManyThrough is inside of Laravel by default and belongsToThrough isn't available. Let's install that package and define that relationship and let's see the amount of queries that we have. We install the package, then we need to define that relationship. So BelongsToThrough we need to use that trait in our Shop model, so we use that trait here and then we can define public function countryFromShop return this belongsToThrough(Country::class, City::class). And now we can try to use the country without any city directly here, and in the index blade we don't need the city either. Let's try it out. Remember there were three queries and 62 models. We refresh, the result is the same and two queries and 59 models. As you can see, city is not present here at all. The query is of course a bit more complex — it's through key something like that — but it helps with performance.
Now let's go even deeper, like literally deeper, with a package called HasManyDeep from the same Jonas Staudenmeyer. Imagine we have the fourth level in the database: employees. Employee belongsTo a Shop which in turn belongsTo a City, City belongsTo a Country. What if you want to query Country with the amount of employees per country? Let's install that package again — this is not available in Laravel Eloquent by default. HasManyDeep allows you to do something like this: hasManyDeep with an array of models, chaining them. Let's try it out. We install the package. Now in our main Country model we should use this trait hasRelationships and then we can define the syntax: public function employees() { return this->hasManyDeep(Employee::class, [City::class, Shop::class]); } So this is the first level, this is the second level. We have City class as the next level and Shop class as another level, and there can be multiple unlimited levels here. We'll take a look at the query a bit later. This is our relationship for country employees. Let's try to do that in the controller. Get back to the previous example of country with count shops, now let's do with countries->withCount('employees') and pass the countries. I've changed the blade to country and employees_count. Let's try it out. Refresh the page, no errors. I've seeded 200 employees and that actually works. This is the query: select countries and then there's subselect join shops join cities and does everything under the hood. So in one query we select everything and we have only three models for the country — so we don't load any cities or shops or even employees; we just load the numbers. If you need all the models of employees of course you can do that with with('employees') instead of withCount; that would do the same result but with more queries and with more models because now you would load all the employees. Again, without intermediate models there's no city model loaded, no shop model loaded — only the query of first level and the last level that you need, with pretty good performance.
Recap, resources, and courses
That's it about deeper levels of Eloquent. To recap: you can use hasManyThrough from Laravel Eloquent or a few packages by Jonas Staudenmeyer, and he has even more packages so you can check out his GitHub profile for more stuff. If you want to know more about Eloquent relationships in general, one of my courses on Teachable is called Laravel Eloquent Expert Level. It's kind of old — it was created I remember in Laravel 6 even, but Eloquent hasn't changed since then. If you want four hours about Eloquent with topics like observers, accessors, mutators, seeds, methods, properties in Eloquent API, all of those relationships as well, collections, relationships — hasManyThrough is mentioned here as one of the lessons. You can check out that course or check out all of my courses on Laravel Daily Teachable.com. I urge you to subscribe to the yearly membership which you will use to get access to all the 20 courses and everything for a year ahead, which I will release the next course I'm planning is on Flutter and Laravel API to create mobile apps. It's probably going to be released in September; I'm not in a rush with that because of some downtime and holiday, but I will keep pushing courses on this Teachable platform and yearly membership will help you to get those. See you guys in other videos.