Introduction

One-to-many is the most common relationship type. One parent has many children: User hasMany Posts, Post hasMany Comments.

Key Concepts

  • hasMany: Defined on the parent; returns a Collection of children when accessed as a property.
  • belongsTo: Defined on the child; returns the single parent.
  • Property vs method access: $user->posts returns a cached Collection; $user->posts() returns a query builder you can chain constraints onto.
  • withCount: Eager-loads a count of related records as a {relation}_count attribute instead of loading the full set.
  • has / whereHas: Filter parents by whether they have related records (or related records matching a condition).

Real World Context

Almost every dashboard query is a hasMany: 'show me this user's orders', 'show me this post's comments'. Understanding property vs method access, and knowing when to reach for withCount or whereHas, is how you keep those queries fast.

Deep Dive

The most common relationship type. One parent record has many child records. For example, a User has many Posts, or a Post has many Comments.

Defining One-to-Many

hasMany (Parent Side)

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class User extends Model
{
    /**
     * Get the user's posts.
     */
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

belongsTo (Child Side)

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Post extends Model
{
    /**
     * Get the user who wrote the post.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Database Structure

users                          posts
┌────┬──────────┐              ┌────┬─────────┬─────────────────┐
│ id │ name     │              │ id │ user_id │ title           │
├────┼──────────┤              ├────┼─────────┼─────────────────┤
│ 1  │ "John"   │◄────────────│ 1  │ 1       │ "First Post"    │
│    │          │◄────────────│ 2  │ 1       │ "Second Post"   │
│ 2  │ "Jane"   │◄────────────│ 3  │ 2       │ "Jane's Post"   │
└────┴──────────┘              └────┴─────────┴─────────────────┘
                                    One user has MANY posts
php
// Get all posts for a user (returns Collection)
$user = User::find(1);
$posts = $user->posts;  // Collection of Post models

foreach ($user->posts as $post) {
    echo $post->title;
}

// Query the relationship
$publishedPosts = $user->posts()
    ->where('published', true)
    ->orderBy('created_at', 'desc')
    ->get();

// Count without loading
$postCount = $user->posts()->count();

// Get parent from child
$post = Post::find(1);
$author = $post->user;  // Returns User model

echo $post->user->name;  // "John"
php
$user = User::find(1);

// Method 1: save() - single record
$post = new Post(['title' => 'My Post', 'body' => '...']);
$user->posts()->save($post);

// Method 2: create() - single record
$post = $user->posts()->create([
    'title' => 'My Post',
    'body' => '...',
]);

// Method 3: saveMany() - multiple records
$user->posts()->saveMany([
    new Post(['title' => 'Post 1']),
    new Post(['title' => 'Post 2']),
]);

// Method 4: createMany() - multiple records
$user->posts()->createMany([
    ['title' => 'Post 1', 'body' => '...'],
    ['title' => 'Post 2', 'body' => '...'],
]);

Updating Parent from Child

php
$post = Post::find(1);

// Associate a different user
$newUser = User::find(2);
$post->user()->associate($newUser);
$post->save();

// Disassociate (set user_id to null)
$post->user()->dissociate();
$post->save();

Relationship Methods vs Properties

php
// Property access - returns cached Collection or null
$posts = $user->posts;  // Collection (cached after first access)
$posts = $user->posts;  // Returns same cached Collection

// Method access - returns query builder
$query = $user->posts();  // Returns HasMany relation (builder)
$query = $user->posts()->where('published', true)->get();

Constraining Eager Loads

php
// Load only published posts
$users = User::with(['posts' => function ($query) {
    $query->where('published', true)
          ->orderBy('created_at', 'desc');
}])->get();

// Shorthand for simple constraints
$users = User::with('posts:id,user_id,title')->get();  // Select specific columns
php
// Eager load count
$users = User::withCount('posts')->get();

foreach ($users as $user) {
    echo $user->posts_count;  // Number of posts
}

// Count with constraints
$users = User::withCount(['posts' => function ($query) {
    $query->where('published', true);
}])->get();

foreach ($users as $user) {
    echo $user->posts_count;  // Published posts only
}

// Multiple counts
$users = User::withCount(['posts', 'comments'])->get();
// $user->posts_count, $user->comments_count

Checking Existence

php
// Has at least one post
$usersWithPosts = User::has('posts')->get();

// Has at least 5 posts
$activeAuthors = User::has('posts', '>=', 5)->get();

// Has posts matching condition
$usersWithPublished = User::whereHas('posts', function ($query) {
    $query->where('published', true);
})->get();

// Doesn't have posts
$usersWithoutPosts = User::doesntHave('posts')->get();

Common Pitfalls

  1. Iterating a hasMany without eager loading — Classic N+1.
  2. Using Collection ops when SQL would be faster — $user->posts->where('published', true) loads everything into memory first; $user->posts()->where('published', true)->get() filters in SQL.
  3. Counting with ->count() instead of withCount — Loads the relationship just to count it.

Best Practices

  1. Eager load with with() when iterating — User::with('posts')->get().
  2. Use withCount() for counts — Adds posts_count without loading the posts themselves.
  3. Use whereHas for filter-by-relationship — User::whereHas('posts', fn ($q) => $q->where('published', true)).

Summary

  • hasMany + belongsTo form the one-to-many pair.
  • $user->posts returns a cached Collection; $user->posts() returns a query builder.
  • withCount loads counts without the full relationship.
  • has / whereHas / doesntHave filter parents by related records.
  • Always eager-load when iterating to avoid N+1.
✓ Completed