Relaciones con modelos Laravel Eloquent a través de otras tablas

  • Por
  • PHP
Cómo acceder a modelos que no están directamente relacionados, relaciones hasManyThrough y la desaparecida belongsToThrough.

Mediante Laravel Eloquent podemos pasar de unas tablas a otras, mediante las relaciones de la base de datos. Podemos configurar los modelos para poder obtener los datos de las tablas relacionadas en la consulta si los necesitamos.

Sin embargo, aunque hemos analizado ya los principales tipos de relaciones y su configuración de los modelos, hasta este punto del Manual de Laravel hemos consultado siempre tablas que estaban directamente relacionadas y nunca tablas que tenían relaciones a través de modelos de otras tablas. En estos casos podemos realizar también la configuración de Eloquent para que el acceso a tablas relacionadas mediante varios pasos se pueda realizar de una manera cómoda.

Tablas de ejemplo para configurar relaciones de varios pasos

Para poder explicar este tipo de relación en Eloquent con ejemplos vamos a usar un modelo de datos como el siguiente.

Tenemos cursos, clases y recursos de las clases. Los cursos tienen muchas clases (1 a N) y las clases tienen muchos recursos (1 a N).

Tabla courses:
- id: clave primaria

Tabla classes:
- id: clave primaria
- course_id: clave foránea

Tabla resources:
- id: clave primaria
- class_id: clave foránea

Relación Has Many Through

En el modelo de datos anterior tenemos una relación a través de otra tabla, desde el modelo de cursos hacia el de recursos. Estos dos modelos no se encuentran relacionados directamente entre sí, pero sin embargo sí que es posible llegar desde los cursos a los recursos, a través de la tabla de clases.

Por tanto, en la estructura de tablas anterior, si queremos saber todos los recursos que pertenecen a un curso (porque el recurso pertenezca a una clase que a su vez pertenece a un curso), tendremos que usar una construcción de Laravel llamada "Has Many Through".

Esta relación es muy sencilla de configurar en el modelo que toca. En este caso la tenemos que configurar en el modelo de cursos, con el método hasManyThrough().

Por ejemplo, en el curso queremos una relación que vamos a llamar "resources", que tendrá el acceso a todos los recursos de las clases de un curso.

public function resources()
{
        return $this->hasManyThrough('App\Resource', 'App\Class');
}

Como puedes ver, haciendo uso del método hasManyThrough, tenemos que indicar dos parámetros. El primero es el modelo de la tabla de destino final y como segundo parámetro indicamos el modelo intermedio.

Si tus tablas no siguen las convenciones del framework, entonces colocaremos más parámetros, para informar de los nombres de modelos, de las claves foráneas y las claves primarias de cada tabla. Por ejemplo:

public function resources()
{
    return $this->hasManyThrough(
      'App\Resource', // Modelo destino
      'App\Class', // Modelo intermedio
      'course' // Clave foránea en la tabla intermedia
      'class' // Clave foránea en la tabla de destino
      'courses_id' // Clave primaria en la tabla de origen
      'classes_id' // Clave primaria en la tabla intermedia
    );
}

Una vez definida la relación en el modelo, podríamos simplemente traernos uno o varios cursos y luego acceder a la relación como si fuera una propiedad normal.

$courses    = Course::all();
dd($courses[1]->resources);

Esto nos mostraría una colección con todos los recursos que tenemos en todas las clases que pertenecen al curso 1.

Como emular un método belongsToThrough

En Laravel no existe el método belongsToThrough, por lo que no podremos hacer la relación justamente inversa, al menos no directamente. Sin embargo hay mecanismos diversos para conseguirlo.

Pueden existir muchas situaciones diferentes en las que quieras pasar de una tabla a otra a través de una intermedia, con tipos de relaciones distintas. Para aclararnos, el caso que estamos queriendo resolver en este ejemplo sería cuando desde el Resource queremos acceder al Course que le pertenece. De nuevo, ambos modelos (course y resource) no están directamente relacionados, pero desde el resource podría acceder a la clase y luego desde la clase acceder al curso. ¿Es posible conseguirlo en un solo paso?

Tendríamos por lo menos dos aproximaciones.

Usar eager loading (carga diligente)

En esta primera aproximación, muy sencilla, podemos simplemente decirle cuando consultamos los recursos, que nos traiga directamente los datos de la clase. Y a su vez, podemos decirle que nos traiga los datos del curso asociado a esa clase.

Simplemente, cuando hacemos la consulta, podemos decirle qué datos queremos recibir por eager loading. Si los datos existentes están vinculados a una tabla intermedia, también se podrían traer.

$resources = Resource::with('class', 'class.course')->get();

En este caso estamos trayendo ya directamente para cada recurso los datos de la clase donde está asociado, y los datos del curso al que pertenece esta clase.

Nota: Como debes de saber, el método "with", estático que ejecutas con Resource::with, indica qué campos relacionados te quieres traer directamente de la consulta. En el caso de "class" estamos indicando que al traer los datos de los recursos, queremos que también se encuentren todas las clases ya en la consulta. Esto se llama "eager loading" y es justamente el comportamiento opuesto al "lazy loading". Por su parte, "class.course" va un poco más allá y lo que está indicando es que, al traerse los recursos, nos entregue también los cursos a los que pertenecen a cada clase. Puedes saber algo más sobre Lazy load y Eager load en el artículo de relaciones en Eloquent.

Para acceder a los datos del curso podríamos hacer algo como esto:

dd($resources[0]->clase->course);

Esto nos mostraría el curso donde está la clase, donde a su vez está el recurso con índice 0. Observa que para acceder al curso necesitas pasar por la clase.

Usar relaciones intermedias

Otra posibilidad que podrías realizar es configurar el modelo para crear una relación con el curso, pasando a través de una relación intermedia definida. Esto nos vendría bien si queremos trabajar mediante carga perezosa, que es el comportamiento predeterminado de las consultas Eloquent con relaciones.

En el ejemplo que nos ocupa, para obtener los datos de un curso determinado al que pertenece un recurso (usando la clase como relación intermedia), podríamos definir la relación de la clase y curso del recurso así:

public function clase()
{
    return $this->belongsTo('App\Class');
}

public function course()
{
    return $this->clase->belongsTo('App\Course');
}

Como puedes comprobar, la relación del recurso con la clase es directa, porque realmente estas tablas sí que están relacionadas con un solo paso. Sin embargo, para definir la relación con el curso tenemos que pasar por la clase. Para conseguir este efecto podemos apoyarnos directamente en la relación de la clase y definir la relación de la clase con el curso.

Para usar esta relación ahora, con carga perezosa, puedes apoyarte en el siguiente código:

$resources   = Resources::all();
dd($resources[0]->course);

Primero me traigo todos los modelos de resources, y sobre el primero de ellos se accede directamente a su curso.

Conclusión

Esperamos que este artículo te haya servido de ayuda cuando quieres implementar relaciones a través de otras tablas en tus modelos. Laravel Eloquent tiene muchos mecanismos para solucionar situaciones similares, que puedes llegar a necesitar cuando tu modelo de datos es otro. Un poquito de experimentar, o googlear, te vendrá bien para resolver otras soluciones más particulares.