Beyond basic where-clause filtering, Eloquent's relationship methods and aggregate functions are what let you query across related tables and summarize data without dropping into raw SQL.
belongsTo
class Post extends Model
{
public function author()
{
return $this->belongsTo(User::class);
}
}
$post->author->name;
belongsTo is the "many" side of a relationship — the model holds the foreign key (user_id on posts, in this example).
hasMany
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
$user->posts; // collection of Post models
hasOne
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class);
}
}
hasOne is functionally like hasMany but returns a single model instead of a collection — used for a genuine one-to-one relationship, like a user having exactly one profile record.
whereHas
$usersWithPublishedPosts = User::whereHas('posts', function ($query) {
$query->where('status', 'published');
})->get();
whereHas filters the parent model based on a condition applied to its related records — this fetches users who have at least one published post, not the posts themselves.
join and leftJoin
$results = DB::table('orders')
->join('users', 'users.id', '=', 'orders.user_id')
->select('orders.*', 'users.name')
->get();
$results = DB::table('users')
->leftJoin('orders', 'users.id', '=', 'orders.user_id')
->select('users.name', 'orders.total')
->get();
A standard join only returns rows with a match on both sides — leftJoin keeps every row from the left table even when there's no matching row on the right (with null for the missing side's columns), which matters when you need to include users with zero orders.
Aggregate methods: count, sum, avg, min, max
$total = Order::count();
$revenue = Order::sum('total');
$average = Order::avg('total');
$cheapest = Order::min('total');
$mostExpensive = Order::max('total');
Each of these executes the aggregate directly in the database rather than pulling every row into PHP and calculating it manually — considerably more efficient for large tables.
Database transactions
DB::transaction(function () {
$order = Order::create([...]);
$order->items()->createMany($itemsData);
Inventory::decrement('stock', $quantity);
});
Wrapping related writes in a transaction ensures they either all succeed or all roll back together — essential any time multiple related records need to stay consistent with each other (an order and its line items, a transfer between two account balances).
Combining relationships with aggregates
$categories = Category::withCount('posts')->get();
// $category->posts_count is now available without an extra query per category
withCount() (and its siblings withSum(), withAvg(), withMin(), withMax()) attach an aggregate of a relationship directly onto each parent model in a single query, avoiding the N+1 query problem that looping and counting manually would introduce.