Eloquent hasMany: Get Parent with Latest Row of Relationship – 5 Ways

https://laraveldaily.com/storage/455/Copy-of-ModelpreventLazyLoading();-(4).png

Eloquent has a lot of “hidden gems”. In this tutorial, let’s see how we can get the latest record from the hasMany Relationship in five different ways.

Let’s imagine we have transactions, and every transaction belongs to a category. The goal is to list the categories with the latest transaction for each of them.

While listing different methods, I will start from the worst one and go to the best method.


Method 1: Blade – Wrong Way (N+1 Query)

The first method is the most straightforward – to take care of the relationship in the Blade. But it’s the worst in terms of performance.

In the Controller, we just get the categories:

class HomeController

{

public function index()

{

$categories = Category::take(10)->get();

 

return view('categories', compact('categories'));

}

}

In the Blade, while viewing those categories, we load the latest transaction and take the first record.

<ul>

@foreach($categories as $category)

<li>

(last transaction: )

</li>

@endforeach

</ul>

The result looks similar to the below image.

latest record result

But with this method, we have an N+1 problem, which makes a database query for every category to get the transaction.

latest record n+1 problem


Method 2: Eager Loading with() – Still Wrong

In the second method, we fix the N+1 problem from the first method. Usually, it is done using the eager loading.

We load the transactions in the Controller using the with method.

class HomeController

{

public function index()

{

$categories = Category::with('transactions')->take(10)->get();

 

return view('categories', compact('categories'));

}

}

And then, in the Blade, we don’t need to query the transactions. We can access the transactions from the collection and sort and get the first from that.

<ul>

@foreach($categories as $category)

<li>

(last transaction: )

</li>

@endforeach

</ul>

But this gives other problems. In the first method, we have ten transactions and categories loaded from the DB.

first method loaded records

After a refresh, we have fewer queries which is great. Well, kinda.

eager loaded records

But now we have loaded all the transactions for each category, which can get heavy on the RAM usage. This means we have improved performance in one place but decreased in another.

many loaded models


Method 3: Subquery

The third method is if you need precisely one field, so you don’t need all the transaction Model, for that, you might write a subquery.

class HomeController

{

public function index()

{

$categories = Category::addSelect(['latest_transaction_amount' => Transaction::select('amount')->whereColumn('category_id', 'categories.id')->latest()->take(1)])

->take(10)

->get();

 

return view('categories', compact('categories'));

}

}

In the query, we named the variable as latest_transaction_amount, so we just can use it in the Blade:

<ul>

@foreach($categories as $category)

<li>

(last transaction: )

</li>

@endforeach

</ul>

This way, we have only one query, but it can only be used when you need only one column from the relation table.

subquery


Method 4: Special Relation Method

But if you need the whole record, there is still a better way. Instead of using a transactions relation in the Category Model, we could create a special relation that would return the latest row.

class Category extends Model

{

// ...

 

public function transactions()

{

return $this->hasMany(Transactions::class);

}

 

public function latest_transaction()

{

return $this->hasOne(Transaction::class)->latest();

}

}

In the Controller, we need to eager load this relationship.

class HomeController

{

public function index()

{

$categories = Category::with('latest_transaction')->take(10)->get();

 

return view('categories', compact('categories'));

}

}

And in the Blade, we don’t need to sort or do anything else.

<ul>

@foreach($categories as $category)

<li>

(last transaction: )

</li>

@endforeach

</ul>

This makes only five queries which are good, but still loads all the transactions.

hasOne latest relationship


Method 5: New latestOfMany()

Since Laravel 8.42, there is a better way. It’s the same thing as latest_transaction in the fourth method, but instead of the latest, we need to use latestOfMany.

class Category extends Model

{

// ...

 

public function transactions()

{

return $this->hasMany(Transactions::class);

}

 

public function latest_transaction()

{

return $this->hasOne(Transaction::class)->latestOfMany();

}

}

So the old query looks like this:

select * from `transactions` where `transactions`.`category_id` in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) order by `created_at` desc

And after a refresh, we have much less loaded Models because we select only one transaction for each category.

hasOne latest of many query

This relation has more methods. You can check them in the Laravel documentation or check the Pull Request at the GitHub.

Laravel News Links