5.15 Relaciones de Paso (HasOneThrough / HasManyThrough) en Laravel
5.15.1 Introducción
En Laravel existen relaciones que permiten acceder a un modelo a través de otro, aunque no exista una relación directa entre ellos. Estas se conocen como relaciones de paso o relaciones "through".
Laravel ofrece dos tipos:
hasOneThrough: una relación uno a uno a través de otro modelo.hasManyThrough: una relación uno a muchos a través de otro modelo.
Estas relaciones son útiles cuando:
- Queremos acceder a un modelo que está relacionado con otro modelo intermedio.
- Evitamos consultas adicionales usando
with()y definiciones en los modelos.
En este tema veremos ambos casos, reutilizando ejemplos anteriores.
5.15.2 Ejemplo 1: hasOneThrough (Usuario → Teléfono → SIMCard)
En este ejemplo partiremos de la solución del tema 12, donde un usuario tiene un teléfono y un telefono pertenece a un usuario. Ahora le añadiremos la tabla sim_cards (tarjetas SIM) y la relación entre teléfonos y tarjetas SIM.
5.15.2.1 Diagrama conceptual
erDiagram
User ||--|| Phone : has
Phone ||--|| SIMCard : has
User ||--|| SIMCard : hasOneThrough
5.15.2.2 Paso 1: Crear el modelo SIMCard
Migración:
Ahora con la migraciòn crearemos la tabla sim_cards:
| Migración SIMCards | |
|---|---|
5.15.2.3 Paso 2: Modificar el modelo Phone
Añadir la clave foránea a la tabla phones:
Añadir sim_card_id a Phone
Modelo Phone:
Modelo SIMCard:
5.15.2.4 Paso 3: Definir relación de paso en User
public function simCard()
{
return $this->hasOneThrough(
SIMCard::class,
Phone::class,
'user_id', // Foreign key on phones
'id', // Foreign key on sim_cards (default is id)
'id', // Local key on users
'sim_card_id' // Local key on phones
);
}
Ahora vamos a crear un seeder para añadir datos a la tabla sim_cards y phones:
| SIMCardSeeder | |
|---|---|
Modificamos el seederde Phone para añadir la relación con SIMCard:
Orden de las migraciones
Además fijate en el orden de ejecución de los seeders. Si es necesario modifica el nombre de la migración de los teléfonos (phones) para que se ejecute después de la migración de las tarjetas SIM (sim_cards). Puedes hacerlo renombrando el archivo de migración o modificando la fecha en el nombre del archivo.
No olvides añadir el seeder al DatabaseSeeder
Además fijate en el orden de ejecución de los seeders. Las tarjetas se tienen que crear antes que los teléfonos. Por lo que el DatabaseSeeder debería quedar así:
Si has cambiado el nombre de alguna migración tendrás que modificar la tabla de migraciones manualmente para que no falle al ejecutar las migraciones. O eliminar manualmente todas las tablas de la base de datos y volver a crearla.
Ahora ejecutamos las migraciones y nos aseguramos que se han creado las tablas y los datos de prueba:
Con esto podemos acceder directamente:
Modificación de la vista:
Ahora modificamos la vista que muestra los datos del usuario para incluir el número de serie de la tarjeta SIM:
@foreach ($users as $user)
<div>
<h2>{{ $user->name }}</h2>
<p>Teléfono: {{ $user->phone->number }}</p>
<p>Tarjeta SIM: {{ $user->simCard->serial_number }}</p>
</div>
@endforeach
Una vez modificada la vista, podemos ver el número de serie de la tarjeta SIM junto al número de teléfono del usuario.
5.15.3 Ejemplo 2: hasManyThrough (Usuario → Teléfonos → SIMCards)
5.15.3.1 Diagrama conceptual
erDiagram
User ||--o{ Phone : has
Phone ||--|| SIMCard : has
User ||--o{ SIMCard : hasManyThrough
Este ejemplo parte del proyecto donde un usuario tiene muchos teléfonos (relación uno a muchos). Cada teléfono tiene una tarjeta SIM.
5.15.3.2 Paso 1: Asegurarnos que la tabla phones tiene sim_card_id
Ya visto en el ejemplo anterior. Si no lo tiene, aplicarlo.
5.15.3.3 Paso 2: Relación de paso en el modelo User
Modificamos las propiedades del modelo User para incluir la relación `hasMany para los teléfonos y la relación hasManyThrough para las tarjetas SIM:
public function phones()
{
return $this->hasMany(Phone::class);
}
public function simCards()
{
return $this->hasManyThrough(
SIMCard::class,
Phone::class,
'user_id', // Foreign key on phones
'id', // Foreign key on sim_cards (default is id)
'id', // Local key on users
'sim_card_id' // Local key on phones
);
}
En el controlador UserController, debemos modificar la lacy eager loading para incluir las tarjetas los teléfonos (ojo en plural), ahora la relación con phones es uno a muchos.
public function index()
{
$users = User::with(['phones'])->get();
return view('users.index', compact('users'));
}
HasManythrough, podemos hacer lo siguiente:
$users = User::with('simCards')->find(1);
foreach ($user->simCards as $sim) {
echo $sim->serial_number;
}
Vamos a modificar nuestra vista para que muestre las SimCards de cada usuario. En el primer bucle seguimos las relaciones de la base de datos, un usuario tiene teléfonos y cada teléfono tiene una tarjeta SIM. En el segundo bucle accedemos a las tarjetas SIM de cada usuario a través de la relación hasManyThrough.
@foreach ($users as $user)
<div>
<h2>{{ $user->name }}</h2>
<p>Teléfonos:</p>
<ul>
@foreach ($user->phones as $phone)
<li>{{ $phone->number }} ({{ $phone->simCard->serial_number }})</li>
@endforeach
</ul>
<p>Tarjetas SIM:</p>
<ul>
@foreach ($user->simCards as $simCard)
<li>{{ $simCard->serial_number }} ({{ $simCard->provider }})</li>
@endforeach
</ul>
</div>
5.15.4 Conclusiones
- Las relaciones
hasOneThroughyhasManyThroughpermiten acceder a modelos intermedios sin necesidad de definir manualmente la lógica. - Requieren entender bien la estructura de claves foráneas.
- Pueden mejorar el rendimiento al evitar consultas adicionales si se usan con
with(). - Son ideales cuando el modelo final depende de una cadena de relaciones lógica.