Adding more subquery scopes0:00
In the last lesson, we learned how to use subqueries to get a single record from a hasMany relationship in the most efficient way. Now we're going to take this technique one step further by using it to create dynamic relationships. Using a subquery to get the last login date was great, but what if we wanted to add additional information about the last login? For example, maybe we want to show the IP address of the last login as well. How would we do this? One option is to simply create another last login subquery scope for the IP address. Let's go to the User model and duplicate our existing withLastLogin scope, and then we'll modify it for our new IPAddress column. And of course, we'll remove the datetime casting because we don't need that for the IP address. And finally, let's update our Blade view. We'll add the IP address after the last login date. User last login IP address. And if we hit refresh in the browser, we can see that the IP address has been added. And while this certainly works, what if we want yet another column from the logins table?
Introducing dynamic relationships0:50
User last login IP address. And if we hit refresh in the browser, we can see that the IP address has been added. And while this certainly works, what if we want yet another column from the logins table? For example, maybe we want the ID of the login to generate a link. Creating one-off subquery scopes each time feels a little tedious to me. Plus, we could end up with a ton of scopes. It would be much nicer if we could just work with an actual Login model like a normal relationship, especially if that model had additional functionality built into it such as helper methods or even its own relationships. Well, it turns out we can. We can do this using dynamic relationships. Let me show you how. We'll start by removing the two last login scopes in our User model. Next, we'll create a new last login belongsTo relationship.
Creating lastLogin relationship1:28
Let me show you how. We'll start by removing the two last login scopes in our User model. Next, we'll create a new lastLogin belongsTo relationship. And that'll just return a belongsTo relationship for our Login model. Now normally for a belongsTo relationship to work, our table needs a column for the foreign key. In our example, that would mean having a last_login_id column on our users table. However, since our users table of course does not have this column, we'll use a subquery to select it instead. Let's create a new withLastLogin scope to do this. And that'll take an instance of the queryBuilder. And then we'll add a select. And then we'll give it the column last_login_id since that's what our lastLogin relationship method expects.
And then we'll add a select. And then we'll give it the column lastLoginId since that's what our lastLogin relationship method expects. And we'll get that from the logins table, selecting the ID. Where the logins.user_id equals the userId of the parent query. Ordering them by the latest. And taking the first one. This is essentially the exact same query that we wrote for the createdAt date and IP address. Except this time we're just grabbing the ID. However, this time we're going to automatically eager load our lastLogin relationship anytime this scope gets called. When using this technique, Eloquent has no idea that the lastLoginId column isn't a real column.
Updating controller and view2:30
However, this time we're going to automatically eager load our lastLogin relationship anytime this scope gets called. When using this technique, Eloquent has no idea that the lastLoginId column isn't a real column. Meaning everything just works as if it is. Let's update our query in the UsersController. Let's remove our two existing scope calls and add a new one for our withLastLogin scope. And finally, let's update our users blade view. Instead of using this lastLoginAt attribute, we'll use a lastLogin relationship instead. And then we'll access the createdAt date directly on that model. And we'll do the same for the IP address. lastLoginIP address.
And we'll do the same for the IP address. Last login IP address. Okay, let's check this out in the browser. If we hit refresh, we can see that everything still works. Let's take a closer look at our queries. We have our first query that gets our User's pagination count. And then we have a second query to actually select the users. And this includes our new dynamic relationship subquery, which gets the ID of the User's last login. And finally, we have an additional third query which eager loads all the last login records for those users. And if we look at our models tab, we're only loading 15 login records and 15 User records.
Limitations and hasOne comparison3:26
And finally, we have an additional third query which eager loads all the last login records for those Users. And if we look at our models tab, we're only loading 15 login records and 15 User records. How cool is that? So one quick thing to be aware of with this technique, you can't lazy load dynamic relationships. And this is because unless we explicitly call the withLastLogin scope, no last login ID will be present on the model. However, I don't find this to be an issue since I tend to use dynamic relationships in situations where I'm already trying to solve a performance issue. And in those situations, lazy loading is generally not the best approach anyway. One last thing before we wrap up. You might be wondering at this point if we could have avoided all this work by simply using a hasOne relationship with an orderBy. And the short answer is no.
You might be wondering at this point if we could have avoided all this work by simply using a hasOne relationship with an orderBy. And the short answer is no. But let's try just to be sure. Let's update our lastLogin relationship to be a hasOne and then order the results. And to test this out, we'll also need to disable our withLastLogin scope. And now if we go back to the browser and hit refresh, we can see that this actually does work. If we look at our models in the Laravel debug bar, we can see that we're still only loading 15 login records and 15 user records. However, if we look at our queries, we can see that we're now back to running 17 queries. We've reintroduced our endPlusOne issue. Let's try fix that by updating our query in the UsersController to eager load the lastLogin relationship.
Why hasOne eager loading fails4:42
We've reintroduced our end plus one issue. Let's try fix that by updating our query in the UserController to eager load the lastLogin relationship. And if we go to the browser and hit refresh, we can see that we're back down to three database queries. However, if we go to our models tab, you can see that we've reintroduced our seven and a half thousand login records issue. And that makes sense since Laravel can't set a limit when eager loading the lastLogin relationship, since it needs to get multiple records, one for each User. And to just underline why Laravel works this way, let's force a limit on our lastLogin relationship. And now if we hit refresh in the browser again, we'll get an error trying to get property created_at of non object. Why is that? Well, it's because our view assumes that each User has a lastLogin record. Let's add a conditional just to make it more clear what's happening here.
Well, it's because our view assumes that each User has a last login record. Let's add a conditional just to make it more clear what's happening here. And if we hit refresh in the browser again, we can see that we're only getting the last login information for one User. And if we review our logins query, we can see why. We're selecting the logins for all 15 of these Users, ordering them by their created_at date, and then applying a limit of one. That means we'll only get the last login record for the User who last logged in and all the rest will be null. So while we can get close with a hasOne relationship, we ultimately still run into the same performance issues as before. And this only further illustrates how powerful using sub queries to create dynamic relationships is.
