Lección 20 de 45 15 min de lectura

Relaciones Uno a Uno y Uno a Muchos

Las relaciones permiten conectar modelos entre sí, reflejando cómo se relacionan las tablas en la base de datos. En esta lección aprenderás las relaciones más comunes: uno a uno (hasOne/belongsTo) y uno a muchos (hasMany/belongsTo).

Conceptos básicos

En bases de datos relacionales, las tablas se conectan mediante claves foráneas (foreign keys). Eloquent te permite definir estas relaciones como métodos en tus modelos, haciendo muy fácil acceder a datos relacionados.

Hay tres tipos principales de relaciones:

  • Uno a uno: Un usuario tiene un perfil
  • Uno a muchos: Un usuario tiene muchos posts
  • Muchos a muchos: Un post tiene muchas etiquetas (siguiente lección)

Relación Uno a Uno (hasOne / belongsTo)

Una relación uno a uno conecta un registro con exactamente otro registro. Por ejemplo, un usuario tiene un único perfil.

Estructura de la base de datos

Primero, necesitas las migraciones con la clave foránea:

php
<?php

// Migración de profiles
Schema::create('profiles', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('bio')->nullable();
    $table->string('avatar')->nullable();
    $table->date('birth_date')->nullable();
    $table->timestamps();
});
foreignId y constrained

foreignId('user_id')->constrained() crea automáticamente una columna user_id de tipo BIGINT UNSIGNED y la relaciona con la tabla users.

Definir la relación en los modelos

En el modelo User, define el método hasOne:

php
<?php

namespace App\Models;

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

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

En el modelo Profile, define la relación inversa con belongsTo:

php
<?php

namespace App\Models;

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

class Profile extends Model
{
    protected $fillable = ['user_id', 'bio', 'avatar', 'birth_date'];

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

Usar la relación

php
<?php

use App\Models\User;
use App\Models\Profile;

// Acceder al perfil de un usuario
$user = User::find(1);
$profile = $user->profile;  // Devuelve el modelo Profile o null

echo $profile->bio;

// Acceder al usuario desde el perfil
$profile = Profile::find(1);
$user = $profile->user;

echo $user->name;

Crear registros relacionados

php
<?php

use App\Models\User;

$user = User::find(1);

// Crear un perfil para el usuario
$profile = $user->profile()->create([
    'bio' => 'Desarrollador web',
    'avatar' => 'avatar.jpg',
]);

// Laravel asigna automáticamente user_id

Relación Uno a Muchos (hasMany / belongsTo)

Una relación uno a muchos conecta un registro con múltiples registros. Por ejemplo, un usuario puede tener muchos posts.

Estructura de la base de datos

php
<?php

// Migración de posts
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('content');
    $table->boolean('is_published')->default(false);
    $table->timestamps();
});

Definir la relación en los modelos

En el modelo User, define el método hasMany:

php
<?php

namespace App\Models;

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

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

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

En el modelo Post, define la relación inversa:

php
<?php

namespace App\Models;

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

class Post extends Model
{
    protected $fillable = ['user_id', 'title', 'slug', 'content', 'is_published'];

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

Usar la relación

php
<?php

use App\Models\User;
use App\Models\Post;

// Obtener todos los posts de un usuario
$user = User::find(1);
$posts = $user->posts;  // Devuelve una Collection de Posts

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

// Contar posts
echo $user->posts()->count();

// Obtener el autor de un post
$post = Post::find(1);
echo $post->user->name;

Filtrar registros relacionados

Puedes encadenar métodos de consulta sobre la relación:

php
<?php

use App\Models\User;

$user = User::find(1);

// Solo posts publicados
$publishedPosts = $user->posts()
    ->where('is_published', true)
    ->get();

// Posts ordenados por fecha
$recentPosts = $user->posts()
    ->orderBy('created_at', 'desc')
    ->take(5)
    ->get();

// Último post
$latestPost = $user->posts()->latest()->first();

Crear registros relacionados

php
<?php

use App\Models\User;

$user = User::find(1);

// Crear un post para el usuario
$post = $user->posts()->create([
    'title' => 'Mi nuevo artículo',
    'slug' => 'mi-nuevo-articulo',
    'content' => 'Contenido del artículo...',
]);

// Crear múltiples posts
$user->posts()->createMany([
    ['title' => 'Post 1', 'slug' => 'post-1', 'content' => '...'],
    ['title' => 'Post 2', 'slug' => 'post-2', 'content' => '...'],
]);

Eager Loading (Carga anticipada)

Por defecto, Eloquent usa lazy loading: las relaciones se cargan solo cuando las accedes. Esto puede causar el problema N+1.

El problema N+1

php
<?php

// MALO: Esto ejecuta N+1 consultas
$posts = Post::all();  // 1 consulta

foreach ($posts as $post) {
    echo $post->user->name;  // 1 consulta por cada post
}
// Si hay 100 posts = 101 consultas

Solución: with()

Usa with() para cargar relaciones de antemano:

php
<?php

// BUENO: Solo 2 consultas sin importar cuántos posts haya
$posts = Post::with('user')->get();

foreach ($posts as $post) {
    echo $post->user->name;  // No ejecuta consulta adicional
}

// Cargar múltiples relaciones
$users = User::with(['profile', 'posts'])->get();

// Cargar relaciones anidadas
$posts = Post::with('user.profile')->get();
Siempre usa Eager Loading

Cuando vas a iterar sobre una colección y acceder a relaciones, siempre usa with(). Es una de las optimizaciones más importantes en Laravel.

Ejemplo práctico: Blog

Veamos un ejemplo completo con User, Profile, Post y Comment:

php
<?php

// Migración de comments
Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->onDelete('cascade');
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->text('body');
    $table->timestamps();
});
php
<?php

// app/Models/Post.php
namespace App\Models;

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

class Post extends Model
{
    protected $fillable = ['user_id', 'title', 'slug', 'content', 'is_published'];

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

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}
php
<?php

// app/Models/Comment.php
namespace App\Models;

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

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

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

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

// Uso en un controlador
use App\Models\Post;

class PostController extends Controller
{
    public function show(string $slug)
    {
        // Cargar post con autor y comentarios (con sus autores)
        $post = Post::with(['user', 'comments.user'])
            ->where('slug', $slug)
            ->firstOrFail();

        return view('posts.show', compact('post'));
    }
}

Resumen

  • hasOne: El modelo actual tiene uno del otro (User hasOne Profile)
  • hasMany: El modelo actual tiene muchos del otro (User hasMany Posts)
  • belongsTo: El modelo actual pertenece a otro (Post belongsTo User)
  • La clave foránea va en la tabla del modelo que usa belongsTo
  • Usa with() para evitar el problema N+1
  • Puedes encadenar métodos de consulta sobre las relaciones

Ejercicios

Ejercicio 1: Relación uno a uno

Crea un modelo Phone con campos number y country_code. Establece una relación uno a uno con User (un usuario tiene un teléfono).

Ver solución
<?php

// Migración
Schema::create('phones', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('number');
    $table->string('country_code', 5);
    $table->timestamps();
});

// app/Models/Phone.php
namespace App\Models;

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

class Phone extends Model
{
    protected $fillable = ['user_id', 'number', 'country_code'];

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

// En User.php, añadir:
use Illuminate\Database\Eloquent\Relations\HasOne;

public function phone(): HasOne
{
    return $this->hasOne(Phone::class);
}

// Uso
$user = User::find(1);
$user->phone()->create([
    'number' => '612345678',
    'country_code' => '+34',
]);

echo $user->phone->number; // 612345678

Ejercicio 2: Relación uno a muchos

Crea un modelo Category con campo name. Modifica Post para que pertenezca a una categoría (una categoría tiene muchos posts).

Ver solución
<?php

// Migración de categories
Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

// Añadir category_id a posts
Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('category_id')
        ->nullable()
        ->constrained()
        ->onDelete('set null');
});

// app/Models/Category.php
namespace App\Models;

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

class Category extends Model
{
    protected $fillable = ['name', 'slug'];

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

// En Post.php, añadir:
use Illuminate\Database\Eloquent\Relations\BelongsTo;

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

// Uso
$category = Category::create(['name' => 'Tecnología', 'slug' => 'tecnologia']);

$post = Post::find(1);
$post->category()->associate($category);
$post->save();

// Posts de una categoría
$techPosts = Category::where('slug', 'tecnologia')
    ->first()
    ->posts;

Ejercicio 3: Eager loading

Escribe una consulta que obtenga los últimos 10 posts publicados con su autor y categoría, optimizada con eager loading.

Ver solución
<?php

use App\Models\Post;

$posts = Post::with(['user', 'category'])
    ->where('is_published', true)
    ->orderBy('created_at', 'desc')
    ->take(10)
    ->get();

// Uso en vista Blade
foreach ($posts as $post) {
    echo $post->title;
    echo $post->user->name;           // Sin consulta adicional
    echo $post->category?->name;      // Sin consulta adicional
}

¿Te está gustando el curso?

Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.

Descubrir cursos premium