Database One-to-Many with two foreign key fields in Laravel
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
Mastering One-to-Many with Dual Foreign Keys in Laravel: A Deep Dive into Relational Modeling
As developers building complex applications with Laravel, one of the most common sticking points is translating abstract Entity Relationship Diagrams (ERDs) into efficient and intuitive Eloquent models. You've correctly identified a common hurdle: modeling a scenario where a central entity (like a Football Match) has relationships to multiple other entities (two Teams), which initially suggests a Many-to-Many relationship, but requires a more specific One-to-Many implementation.
This post will walk you through how to structure your database schema and implement the necessary Eloquent relationships to handle a "One-to-Many" scenario involving two distinct foreign keys, solving the exact problem of retrieving all matches played by a specific team efficiently.
The Challenge: Modeling Dual Relationships
You are dealing with three primary entities: Teams and Matches. The complexity arises because a single Match record must link to two distinct Team records—the home team (local) and the away team (visitor). Simply defining a standard one-to-many relationship won't suffice directly, as you need to reference two different parent entities from the child table.
The confusion often stems from trying to force this into a single Eloquent relationship definition. The solution lies in correctly structuring your database schema first, and then leveraging Eloquent’s power to query those relationships effectively.
Step 1: Database Schema Design (Migrations)
Instead of relying on a complex intermediate table for this specific scenario, the most performant and straightforward approach is to embed the necessary foreign keys directly into the matches table. This establishes clear, direct relationships that are easy for the database to index and query.
We start by defining our two core tables: teams and matches.
// database/migrations/..._create_teams_table.php
Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
// database/migrations/..._create_matches_table.php
Schema::create('matches', function (Blueprint $table) {
$table->id();
$table->foreignId('local_team_id')->constrained('teams')->onDelete('cascade');
$table->foreignId('visitor_team_id')->constrained('teams')->onDelete('cascade');
$table->timestamps();
});
By defining local_team_id and visitor_team_id directly on the matches table, we clearly define that each match is intrinsically linked to two teams. This structure adheres to best practices for relational data modeling, which is a core concept emphasized in frameworks like Laravel.
Step 2: Implementing Eloquent Models
Next, we define our Eloquent models and establish the relationships. Since the relationship flows from matches back to two different teams, we must define two separate relationships on the Match model pointing to the Team model.
In your app/Models/Match.php:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Match extends Model
{
public function localTeam(): BelongsTo
{
// Relationship to the team playing at home
return $this->belongsTo(Team::class, 'local_team_id');
}
public function visitorTeam(): BelongsTo
{
// Relationship to the team playing away
return $this->belongsTo(Team::class, 'visitor_team_id');
}
}
Step 3: Solving the Retrieval Challenge
Now we tackle your primary goal: retrieving all matches for a specific team. While the direct relationship above only tells you about one match's teams, to find all matches played by a team, we need to query the matches table and join it with the teams table twice—once for the local ID and once for the visitor ID.
This is where raw Eloquent queries or custom scopes shine, offering maximum performance over complex nested relationships when dealing with dual foreign keys.
To retrieve all matches played by a specific team (e.g., Team ID 1):
use App\Models\Match;
use App\Models\Team;
$team = Team::find(1);
// Retrieve all matches where the team is either local or visitor
$allMatches = Match::where('local_team_id', $team->id)
->orWhere('visitor_team_id', $team->id)
->get();
For a more advanced, cleaner approach that integrates this logic into your model (a pattern often seen in high-level Laravel applications), you can define a scope on the Team model to handle this complex query:
// In App\Models\Team.php
public function matches()
{
// Define a scope to find all matches where this team is either local or visitor
return $this->hasMany(Match::class, 'local_team_id')
->orWhereHas('matches', function ($query) {
$query->where('visitor_team_id', $this->id);
});
}
This approach ensures that when you call $team->matches(), Eloquent efficiently executes the necessary joins across your tables, providing exactly the data you need without forcing an artificial Many-to-Many relationship where one doesn't logically exist. Remember, understanding how to structure your schema correctly is the foundation of writing elegant code in Laravel, allowing you to build robust applications based on sound relational principles.