Introduction

A one-to-one relationship links exactly one parent record to one child. Classic examples: User and Profile, Order and Payment, Company and Settings.

Key Concepts

  • hasOne: Defined on the parent; points to a child that holds the foreign key.
  • belongsTo: Defined on the child; points back to the parent via the foreign key.
  • Foreign key placement: The FK always lives on the belongsTo side — the child's table holds parent_id.
  • withDefault: A helper that returns an empty-but-valid related model instead of null when the relationship is absent.

Real World Context

Most applications have at least one 1:1 pair — user/profile, order/payment, company/settings. Knowing which side gets hasOne vs belongsTo (it's about foreign key location, not conceptual 'ownership') is the first thing to internalize.

Deep Dive

A one-to-one relationship links one record to exactly one other record. For example, a User might have one Profile.

Defining One-to-One

hasOne (Parent Side)

php
<?php

namespace App\Models;

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

class User extends Model
{
    /**
     * Get the user's profile.
     */
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }
}

belongsTo (Child Side)

php
<?php

namespace App\Models;

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

class Profile extends Model
{
    /**
     * Get the user that owns the profile.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Database Structure

users                          profiles
┌────┬──────────┐              ┌────┬─────────┬─────────────┐
│ id │ name     │              │ id │ user_id │ bio         │
├────┼──────────┤              ├────┼─────────┼─────────────┤
│ 1  │ "John"   │◄────────────│ 1  │ 1       │ "Developer" │
│ 2  │ "Jane"   │◄────────────│ 2  │ 2       │ "Designer"  │
└────┴──────────┘              └────┴─────────┴─────────────┘

Migration for Profile

php
Schema::create('profiles', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->text('bio')->nullable();
    $table->string('website')->nullable();
    $table->string('location')->nullable();
    $table->timestamps();
});

Using the Relationship

php
// Access profile from user
$user = User::find(1);
$profile = $user->profile;  // Returns Profile model or null

echo $user->profile->bio;   // "Developer"

// Access user from profile
$profile = Profile::find(1);
$user = $profile->user;     // Returns User model

echo $profile->user->name;  // "John"
php
// Method 1: Create and associate
$user = User::find(1);
$profile = new Profile(['bio' => 'Developer']);
$user->profile()->save($profile);

// Method 2: Create directly
$user->profile()->create(['bio' => 'Developer']);

// Method 3: From the child side
$profile = new Profile(['bio' => 'Developer']);
$profile->user()->associate($user);
$profile->save();

Customizing Keys

php
// Custom foreign key
public function profile(): HasOne
{
    return $this->hasOne(Profile::class, 'author_id');
}

// Custom local key
public function profile(): HasOne
{
    return $this->hasOne(Profile::class, 'user_id', 'user_uuid');
}

// On the belongsTo side
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'author_id');
}

One-to-One with Default

Return a default model if none exists:

php
public function profile(): HasOne
{
    return $this->hasOne(Profile::class)->withDefault();
}

// With default attributes
public function profile(): HasOne
{
    return $this->hasOne(Profile::class)->withDefault([
        'bio' => 'No bio provided',
    ]);
}

// Usage - never returns null
$user->profile->bio;  // "No bio provided" if no profile exists

hasOne vs belongsTo

User (parent)           Profile (child)
┌─────────────┐         ┌─────────────────┐
│             │ hasOne  │                 │
│   User      │────────►│    Profile      │
│             │         │                 │
│             │◄────────│   user_id FK    │
│             │belongsTo│                 │
└─────────────┘         └─────────────────┘

- hasOne: "I have one of these" (User has one Profile)
- belongsTo: "I belong to this" (Profile belongs to User)
- Foreign key is on the belongsTo side (profiles.user_id)

Practical Example

php
// In a controller
public function show(User $user)
{
    // Lazy load (makes 2 queries)
    return view('users.show', [
        'user' => $user,
        'profile' => $user->profile,
    ]);

    // Better: Eager load (makes 1 query)
    $user = User::with('profile')->findOrFail($user->id);

    return view('users.show', compact('user'));
}

// In Blade
<h1>{{ $user->name }}</h1>
<p>{{ $user->profile?->bio ?? 'No bio' }}</p>

Common Pitfalls

  1. Putting the FK on the wrong side — If users.profile_id instead of profiles.user_id, Eloquent's inference breaks.
  2. Accessing a null relationship without ?-> or withDefault() — $user->profile->bio crashes when the profile doesn't exist.
  3. Lazy-loading relationships in a loop — Classic N+1.

Best Practices

  1. Place the FK on the belongsTo side — Profile belongs to User, so profiles.user_id is the FK.
  2. Use ?-> or withDefault() for optional sides — Prevents null crashes in templates.
  3. Eager-load when iterating — User::with('profile')->get() collapses N+1 to 2 queries.

Summary

  • hasOne on the parent, belongsTo on the child, FK always on the child.
  • Access via $user->profile (collection-of-one) or $profile->user (parent).
  • Create via $user->profile()->create([...]) or $profile->user()->associate($user).
  • Custom keys pass as extra arguments to hasOne / belongsTo.
  • withDefault() gives you an empty-but-valid model when the relationship is absent.
✓ Completed