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
// 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('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
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
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
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
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
// 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
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
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
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
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
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
// 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
// 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();
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
// 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
// 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
// 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
// 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
}
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Te está gustando el curso?
Tenemos cursos premium con proyectos reales, soporte personalizado y certificado.
Descubrir cursos premium