Laravel

Eloquent Relationships Explained: One-to-One, One-to-Many, Many-to-Many and More

A practical guide to Laravel Eloquent relationships with clear database examples, model definitions, eager loading patterns, pivot table operations, and N+1 query prevention.

Eloquent relationships are where Laravel starts to feel powerful. Instead of manually joining tables or passing foreign keys around your code, you describe how models relate to each other, then work with those relationships through expressive PHP methods.

This guide explains Laravel Eloquent relationships with practical examples: users and profiles, users and posts, users and roles, countries and posts through users, and comments that can belong to more than one model type. The focus is not only on syntax. It is on choosing the right relationship, understanding the database columns behind it, and avoiding common performance mistakes.

If you are still building your Laravel foundation, start with the Laravel Complete Guide. If your local app is not ready yet, follow How to Install Laravel. If your models are loaded through controller routes, Laravel Routing Explained will help connect the request flow. For production issues, keep How to Fix Common Laravel 500 Server Errors close by.

What Are Eloquent Relationships?

Eloquent relationships are methods on Laravel model classes that describe how database records are connected. A user has one profile. A user has many posts. A post belongs to one user. A user belongs to many roles. A comment may belong to a post, video, or product.

The database still matters. Eloquent does not remove foreign keys, pivot tables, indexes, or good schema design. It gives you a clean object-oriented layer over those database relationships.

  • hasOne: one parent record owns one child record.
  • hasMany: one parent record owns multiple child records.
  • belongsTo: the child record points back to its parent.
  • belongsToMany: two models are connected through a pivot table.
  • hasManyThrough: one model reaches another model through an intermediate model.
  • morphMany and related polymorphic methods: one relationship can point to multiple model types.

How Eloquent Relationships Work

An Eloquent relationship is a model method that returns a relationship object. That object can be used as a query builder, and the relationship can also be accessed as a dynamic property.

use AppModelsUser;

$user = User::find(1);

// Dynamic relationship property: loads the related records.
$posts = $user->posts;

// Relationship method: lets you continue building a query.
$publishedPosts = $user->posts()
    ->where('is_published', true)
    ->latest()
    ->get();

The difference between $user->posts and $user->posts() matters. The property gives you the related data. The method gives you the relationship query.

One-to-One Relationships

A one-to-one relationship means one record is associated with exactly one related record. A practical example is a user profile. The users table stores authentication-level data, while the profiles table stores extra profile details.

Database shape:

  • users.id is the parent key.
  • profiles.user_id is the foreign key.
  • Each profile belongs to one user.
Schema::create('profiles', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('headline')->nullable();
    $table->text('bio')->nullable();
    $table->timestamps();
});
<?php

namespace AppModels;

use IlluminateDatabaseEloquentRelationsHasOne;
use IlluminateFoundationAuthUser as Authenticatable;

class User extends Authenticatable
{
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }
}
<?php

namespace AppModels;

use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsBelongsTo;

class Profile extends Model
{
    protected $fillable = ['headline', 'bio'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Now you can access the profile from the user:

$user = User::with('profile')->findOrFail($id);

return view('users.show', [
    'user' => $user,
    'headline' => $user->profile?->headline,
]);

One-to-Many Relationships

A one-to-many relationship means one parent record can have many child records. A user can write many posts, but each post belongs to one user.

Database shape:

  • users.id is the parent key.
  • posts.user_id stores the author.
  • One user can be connected to many posts.
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('body');
    $table->boolean('is_published')->default(false);
    $table->timestamps();
});
use IlluminateDatabaseEloquentRelationsHasMany;

public function posts(): HasMany
{
    return $this->hasMany(Post::class);
}
use IlluminateDatabaseEloquentRelationsBelongsTo;

public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}

A controller can load a user and show the user’s posts:

$user = User::with(['posts' => function ($query) {
    $query->where('is_published', true)->latest();
}])->findOrFail($id);

return view('users.posts', compact('user'));

Many-to-Many Relationships

A many-to-many relationship means records on both sides can connect to many records on the other side. Users and roles are the classic example: a user can have many roles, and a role can belong to many users.

Database shape:

  • users stores users.
  • roles stores roles.
  • role_user is the pivot table that connects them.
  • role_user.user_id points to users.id.
  • role_user.role_id points to roles.id.
Schema::create('roles', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->timestamps();
});

Schema::create('role_user', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->foreignId('role_id')->constrained()->cascadeOnDelete();
    $table->timestamps();

    $table->primary(['user_id', 'role_id']);
});
use IlluminateDatabaseEloquentRelationsBelongsToMany;

public function roles(): BelongsToMany
{
    return $this->belongsToMany(Role::class)->withTimestamps();
}
use IlluminateDatabaseEloquentRelationsBelongsToMany;

public function users(): BelongsToMany
{
    return $this->belongsToMany(User::class)->withTimestamps();
}

Laravel expects the pivot table name to use the related model names in alphabetical order by convention. For Role and User, that table is role_user.

BelongsTo Relationships

belongsTo is the inverse side of a relationship. If a user has many posts, a post belongs to a user. The foreign key usually lives on the model that belongs to the parent.

class Post extends Model
{
    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'user_id');
    }
}

The relationship method name can be user or author. If the method name does not match the foreign key convention, pass the foreign key explicitly, as shown above.

HasOne and HasMany

Use hasOne when the parent should have one related record. Use hasMany when the parent can have multiple related records.

  • User has one profile: hasOne(Profile::class).
  • User has many posts: hasMany(Post::class).
  • Post has many comments: hasMany(Comment::class).
  • Order has one invoice: hasOne(Invoice::class).

If you are unsure which one to use, look at the database. Can the child table contain multiple rows with the same parent foreign key? If yes, it is usually hasMany. If it should contain only one row per parent, it is usually hasOne and you may also want a unique constraint on the foreign key.

$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();

HasManyThrough

hasManyThrough lets one model access related records through an intermediate model. A practical example: a country has many posts through users.

Database shape:

  • countries.id connects to users.country_id.
  • users.id connects to posts.user_id.
  • The country does not have a direct foreign key on posts, but it can reach posts through users.
use IlluminateDatabaseEloquentRelationsHasManyThrough;

class Country extends Model
{
    public function posts(): HasManyThrough
    {
        return $this->hasManyThrough(Post::class, User::class);
    }
}
$country = Country::with('posts')->where('code', 'PK')->firstOrFail();

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

Use this relationship when the connection is real and stable. If the query is highly custom or crosses several business rules, a query object or explicit query may be clearer.

Polymorphic Relationships

A polymorphic relationship lets one model belong to more than one type of parent model. Comments are a realistic example. You may want comments on posts and products without creating separate post_comments and product_comments tables.

Database shape:

  • comments.commentable_id stores the parent record ID.
  • comments.commentable_type stores the parent model type.
  • A comment can belong to a post, product, or another commentable model.
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    $table->morphs('commentable');
    $table->timestamps();
});
use IlluminateDatabaseEloquentRelationsMorphMany;

class Post extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Product extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
use IlluminateDatabaseEloquentRelationsMorphTo;

class Comment extends Model
{
    protected $fillable = ['body'];

    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

Now both posts and products can create comments through the same relationship name:

$post->comments()->create([
    'body' => 'This explanation helped.',
]);

$product->comments()->create([
    'body' => 'Does this product support team billing?',
]);

Polymorphic relationships are useful, but they can be harder to query and enforce at the database level than normal foreign keys. Use them when the shared behavior is worth that tradeoff.

Eloquent Relationship Methods

Relationship methods are more than accessors. They return query-capable relationship objects, so you can filter, count, create, attach, detach, and sync related data.

// Get all related posts.
$posts = $user->posts;

// Query related posts.
$drafts = $user->posts()
    ->where('is_published', false)
    ->get();

// Count related records without loading all models.
$postCount = $user->posts()->count();

// Create a related record with the foreign key filled automatically.
$post = $user->posts()->create([
    'title' => 'Eloquent Relationships Explained',
    'slug' => 'eloquent-relationships-explained',
    'body' => '...',
]);

If you need to add query constraints, call the method with parentheses. If you need already loaded related data, use the property.

Loading Relationships

Loading strategy is where many beginners accidentally create slow pages. The code may look clean while quietly running dozens or hundreds of extra queries.

Lazy Loading

Lazy loading happens when you access a relationship property and Laravel loads it only at that moment.

$post = Post::first();

// This triggers a query if the user relationship is not loaded yet.
echo $post->user->name;

Lazy loading is convenient for simple cases, but it can create N+1 query problems inside loops.

Eager Loading with with()

Eager loading tells Laravel to load relationships up front.

$posts = Post::with('user')->latest()->get();

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

Without eager loading, Laravel may run one query for posts, then one query per post to load the user. With eager loading, it can load the posts and their users in a small, predictable number of queries.

Lazy Eager Loading with load()

Use load() when you already have a model or collection and then decide you need relationships.

$user = User::findOrFail($id);

$user->load(['profile', 'posts' => function ($query) {
    $query->latest()->limit(5);
}]);

Preventing N+1 Query Problems

An N+1 query problem happens when your code runs one query to fetch a list and then one additional query for each item in the list.

// Problem: can trigger one extra user query per post.
$posts = Post::latest()->get();

foreach ($posts as $post) {
    echo $post->user->name;
}
// Better: load users with the posts.
$posts = Post::with('user')->latest()->get();

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

Laravel can also be configured during development to prevent lazy loading, which helps catch accidental N+1 issues earlier.

use IlluminateDatabaseEloquentModel;

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

Accessing Relationship Data

Once relationships are defined, you can use them naturally in controllers and Blade views. Keep nullability in mind. A user may not have a profile yet, or a post may have an optional relationship.

$post = Post::with(['user.profile', 'comments'])->findOrFail($id);

return view('posts.show', [
    'post' => $post,
]);
<h1>{{ $post->title }}</h1>

<p>By {{ $post->user->name }}</p>

@if ($post->user->profile)
    <p>{{ $post->user->profile->headline }}</p>
@endif

@foreach ($post->comments as $comment)
    <article>{{ $comment->body }}</article>
@endforeach

For optional one-to-one relationships, PHP’s null-safe operator is often cleaner in controllers:

$headline = $user->profile?->headline;

Creating and Updating Related Records

Create related records through the relationship when you want Laravel to fill the foreign key automatically.

$user = User::findOrFail($id);

$user->profile()->create([
    'headline' => 'Laravel developer',
    'bio' => 'Building practical web applications.',
]);
$post = $user->posts()->create([
    'title' => 'My First Laravel Post',
    'slug' => 'my-first-laravel-post',
    'body' => '...',
    'is_published' => false,
]);

To update a related one-to-one record, handle the case where it may not exist yet:

$user->profile()->updateOrCreate(
    ['user_id' => $user->id],
    [
        'headline' => 'Senior Laravel developer',
        'bio' => 'Focused on maintainable Laravel applications.',
    ],
);

Make sure the related model has the correct $fillable fields or uses another intentional mass-assignment strategy.

Attaching and Detaching Many-to-Many Records

Many-to-many relationships use a pivot table, so Laravel provides methods for managing those connections.

attach()

attach() adds a new pivot table row.

$user = User::findOrFail(1);

$user->roles()->attach($adminRoleId);

You can also attach pivot data:

$user->roles()->attach($editorRoleId, [
    'assigned_by' => auth()->id(),
]);

detach()

detach() removes pivot table rows.

// Remove one role.
$user->roles()->detach($editorRoleId);

// Remove all roles from the user.
$user->roles()->detach();

sync()

sync() makes the pivot table match the exact IDs you provide. It attaches missing records and detaches records that are not in the new list.

$user->roles()->sync([1, 2, 3]);

Use sync() for forms where the submitted role IDs should become the user’s complete role set. If you only want to add without removing existing roles, use syncWithoutDetaching().

$user->roles()->syncWithoutDetaching([$viewerRoleId]);

Common Mistakes

  • Using hasMany on the child model when the child should use belongsTo.
  • Forgetting the foreign key column, such as posts.user_id.
  • Naming a relationship one thing while relying on a different foreign key convention.
  • Using $user->posts when you need to add query constraints with $user->posts().
  • Creating N+1 query problems by accessing relationships inside loops without eager loading.
  • Using a many-to-many relationship without creating the pivot table.
  • Forgetting withTimestamps() when the pivot table has timestamps.
  • Using polymorphic relationships for cases where a normal foreign key would be simpler and safer.
  • Not indexing foreign key columns used heavily in relationship queries.
  • Assuming Eloquent will fix a poor database design.

Eloquent Relationship Performance

Relationship performance usually comes down to loading only what you need and avoiding repeated queries. Eager loading solves many N+1 problems, but eager loading everything can also waste memory and database time.

// Load only the relationships needed for this screen.
$posts = Post::query()
    ->with(['user:id,name', 'comments:id,post_id,body'])
    ->latest()
    ->paginate(15);

When counting related records, use withCount() instead of loading entire collections just to count them.

$users = User::withCount('posts')->get();

foreach ($users as $user) {
    echo $user->posts_count;
}

For large pages, combine eager loading with pagination. A relationship that works well for 10 records may become expensive with 10,000.

Best Practices

  • Design the database relationship first, then write the Eloquent relationship.
  • Use clear relationship names: posts, author, roles, comments.
  • Use belongsTo on the model that contains the foreign key.
  • Use eager loading on list pages that display related data.
  • Use relationship methods when adding query constraints or creating related records.
  • Use dynamic relationship properties when reading already loaded relationship data.
  • Add indexes and foreign keys where they make sense for integrity and query performance.
  • Keep polymorphic relationships for cases where multiple parent model types genuinely share the same child behavior.
  • Use withCount() for counts instead of loading full related collections.
  • Prefer small, explicit relationship queries over clever chains that future developers will struggle to read.

Related reading: Laravel Migrations Best Practices.

Frequently Asked Questions

What is the difference between hasMany and belongsTo?

hasMany goes from the parent model to multiple child models. belongsTo goes from the child model back to the parent model. If the current table contains the foreign key, the model often uses belongsTo.

Where should the foreign key go in a one-to-many relationship?

The foreign key goes on the many side. For users and posts, posts.user_id points to users.id.

What table is needed for belongsToMany?

A many-to-many relationship needs a pivot table. For users and roles, the conventional table name is role_user with user_id and role_id columns.

What causes N+1 queries in Eloquent?

N+1 queries happen when you load a list of models and then lazily load a relationship for each model in a loop. Use eager loading with with() when a page needs related data for many records.

Should I always eager load relationships?

No. Eager load relationships needed by the current screen or response. Loading unnecessary relationships can waste memory and database work.

When should I use polymorphic relationships?

Use polymorphic relationships when one child model naturally belongs to several different parent model types, such as comments on posts and products. Avoid them when a normal relationship would be clearer.

Conclusion

Laravel Eloquent relationships are not just convenient syntax. They are a way to make your database design visible in your application code. When relationships are named clearly and backed by sensible tables, controllers become smaller, queries become easier to read, and related data becomes much easier to work with.

Start with the database structure: where is the foreign key, can there be one related record or many, and do you need a pivot table? Then choose the matching Eloquent relationship. Use eager loading when displaying related data in lists, watch for N+1 queries, and keep polymorphic relationships for cases where they genuinely simplify the model.

FAQ

What is the difference between hasMany and belongsTo?

hasMany goes from the parent model to multiple child models. belongsTo goes from the child model back to the parent model. If the current table contains the foreign key, the model often uses belongsTo.

Where should the foreign key go in a one-to-many relationship?

The foreign key goes on the many side. For users and posts, posts.user_id points to users.id.

What table is needed for belongsToMany?

A many-to-many relationship needs a pivot table. For users and roles, the conventional table name is role_user with user_id and role_id columns.

What causes N+1 queries in Eloquent?

N+1 queries happen when you load a list of models and then lazily load a relationship for each model in a loop. Use eager loading with with() when a page needs related data for many records.

Should I always eager load relationships?

No. Eager load relationships needed by the current screen or response. Loading unnecessary relationships can waste memory and database work.

When should I use polymorphic relationships?

Use polymorphic relationships when one child model naturally belongs to several different parent model types, such as comments on posts and products. Avoid them when a normal relationship would be clearer.

Sources

  1. https://laravel.com/docs/12.x/eloquent-relationships
  2. https://laravel.com/docs/12.x/eloquent
  3. https://laravel.com/docs/12.x/migrations

Get the weekly builder briefing