Frequentemente, em algum ponto da carreira de qualquer desenvolvedor, é necessário interagir com um banco de dados. É nesse momento que o Eloquent, o mapeador objeto-relacional (ORM) do Laravel, torna o processo de interação com suas tabelas do banco de dados intuitivo e natural.

É crucial que, como profissional, você reconheça e compreenda os seis principais tipos de relacionamentos, os quais abordaremos e revisaremos.

O que são relacionamentos no Eloquent?

Ao trabalhar com tabelas em um banco de dados relacional, podemos caracterizar os relacionamentos como conexões entre tabelas. Isso ajuda você a organizar e estruturar os dados sem esforço, permitindo maior legibilidade e manuseio dos dados. Na prática, existem três tipos de relacionamentos do banco de dados:

  • um-para-um – Um registro em uma tabela está associado a um, e somente um, em outra tabela. Por exemplo, uma pessoa e um número de seguro social.
  • um-para-muitos – Um registro está associado a vários registros em outra tabela. Por exemplo, um escritor e seus blogs.
  • muitos-para-muitos – Vários registros em uma tabela são associados a vários registros em outra tabela. Por exemplo, alunos e os cursos em que estão matriculados.

Com o Laravel, você pode interagir e gerenciar facilmente os relacionamentos do banco de dados usando a sintaxe orientada a objetos no Eloquent.

Além dessas definições, o Laravel introduz outros tipos de relacionamentos, como:

  • Has Many Through (Tem Muitos Através)
  • Polymorphic Relations (Relações Polimórficas)
  • Many-to-many Polymorphic (Muitos-para-muitos Polimórficos)

Por exemplo, considere uma loja cujo estoque contém uma variedade de artigos, cada um em sua própria categoria. Portanto, dividir o banco de dados em várias tabelas faz sentido do ponto de vista comercial. No entanto, isso vem com seus próprios desafios, já que você não deseja consultar cada tabela individualmente.

Podemos criar facilmente uma relação simples de um-para-muitos no Laravel para nos ajudar, como quando precisamos consultar os produtos, podemos fazer isso usando o modelo Product.

Schema do banco de dados com três tabelas e uma tabela conjunta representando um relacionamento
Schema do banco de dados com três tabelas e uma tabela conjunta representando um relacionamento polimórfico.

Relacionamento um-para-um

Sendo a primeira relação básica que o Laravel oferece, ela associa duas tabelas de forma que uma linha da primeira tabela esteja correlacionada com apenas uma linha da outra tabela.

Para ver isso em ação, temos de criar dois modelos com sua própria migração:

php artisan make:model Tenant 
Php artisan make:model Rent

Neste ponto, temos dois modelos: um é o Tenant (locatário) e o outro é o Rent (aluguel).

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * Get the rent of a Tenant
    */
    public function rent() 
    {
        return $this->hasOne(Rent::class);
    }
}

Como o Eloquent determina o relacionamento de chave estrangeira com base no nome do modelo principal (Tenant, nesse caso), o modelo Rent pressupõe que existe uma chave estrangeira tenant_id.

Você pode substituí-la facilmente com um argumento adicional para o método hasOne:

return $this- >hasOne(Rent::class, "custom_key");

O Eloquent também assume que existe uma correspondência entre a chave estrangeira definida e a chave primária do principal (modelo Tenant). Por padrão, ele procurará fazer a correspondência entre tenant_id e a chave id do registro Tenant. Podemos substituir isso por um terceiro argumento no método hasOne, de modo que ele corresponda a outra chave:

return $this->hasOne(Rent::class, "custom_key", "other_key"); 

Agora que definimos a relação de um-para-um entre os modelos, podemos usá-la facilmente, assim:

$rent = Tenant::find(10)->rent;

Com essa linha de código, obtemos o aluguel do locatário com o ID 10, caso ele exista.

Relacionamento um-para-muitos

Assim como o relacionamento anterior, este definirá relacionamentos entre um modelo principal único e vários modelos secundários. É improvável que nosso locatário tenha apenas uma conta de aluguel, pois se trata de um pagamento recorrente, portanto, ele terá vários pagamentos.

Nesse caso, nosso relacionamento anterior tem falhas, e podemos corrigi-las:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tenant extends Model
{
    /**
    * Get the rents of a Tenant
    */
    public function rent() 
    {
        return $this->hasMany(Rent::class);
    }
}

Antes de chamarmos o método para obter os aluguéis, é bom saber que as relações servem como construtores de consultas, portanto, podemos adicionar restrições (como aluguel entre datas, pagamento mínimo etc.) e encadeá-las para obter o resultado desejado:

$rents = Tenant::find(10)->rent()->where('payment', '>', 500)->first();

E, como na relação anterior, podemos substituir as chaves estrangeiras e locais passando argumentos adicionais:

return $this->hasMany(Rent::class, "foreign_key");
return $this->hasMany(Rent::class, "foreign_key", "local_key");

Agora temos todos os aluguéis de um locatário, mas o que fazemos quando sabemos o valor do aluguel e queremos descobrir a quem ele pertence? Podemos usar a propriedade belongsTo:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * Return the tenant for the rent
    */
    public function tenant() 
    {
        return $this->belongsTo(Tenant::class);
    }
}

E agora podemos obter o locatário facilmente:

$tenant = Rent::find(1)->tenant;

Para o método belongsTo, também podemos substituir as chaves estrangeiras e locais, como fizemos anteriormente.

Relacionamento Has-One-Of-Many

Como nosso modelo Tenant pode ser associado a muitos modelos Rent, queremos recuperar facilmente o modelo relacionado mais recente ou mais antigo dos relacionamentos.

Uma maneira conveniente de fazer isso é combinar os métodos hasOne e ofMany:

public function latestRent() {
    return $this->hasOne(Rent::class)->latestOfMany();
}

public function oldestRent() {
    return $this->hasOne(Rent::class)->oldestOfMany();
}

Por padrão, estamos obtendo os dados com base na chave primária, que pode ser classificada, mas podemos criar nossos próprios filtros para o método ofMany:

return $this->hasOne(Rent::class)->ofMany('price', 'min');

Relacionamentos HasOneThrough e HasManyThrough

Os métodos -Through sugerem que nossos modelos terão de passar por outro modelo para estabelecer um relacionamento com o modelo desejado. Por exemplo, podemos associar o Aluguel ao Locador, mas o Aluguel deve passar primeiro pelo Locatário para chegar ao Locador.

As chaves das tabelas necessárias para isso seriam as seguintes:

rent
    id - integer
    name - string
    value - double

tenants
    id - integer
    name - string
    rent_id - integer

landlord
    id - integer
    name - string
    tenant_id - integer

Após visualizar a aparência de nossas tabelas, podemos criar os modelos:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Rent extends Model
{
    /**
    * Return the rents' landlord
    */
    public function rentLandlord() 
    {
        return $this->hasOneThrough(Landlord::class, Tenant::class);
    }
}

O primeiro argumento do método hasOneThrough é o modelo que você deseja acessar, e o segundo argumento é o modelo pelo qual você passará.

E, assim como antes, você pode substituir as chaves estrangeiras e locais. Agora que temos dois modelos, temos dois de cada para substituir nesta ordem:

public function rentLandlord() 
{
    return $this->hasOneThrough(
        Landlord::class,
        Tenant::class,
        "rent_id",    // Foreign key on the tenant table
        "tenant_id",  // Foreign key on the landlord table
        "id",         // Local key on the tenant class
        "id"          // Local key on the tenant table
    );
}

Da mesma forma, o relacionamento “Has Many Through” no Laravel Eloquent é útil quando você deseja acessar registros em uma tabela distante por meio de uma tabela intermediária. Vamos considerar um exemplo com três tabelas:

  • país
  • usuários
  • jogos

Cada país tem muitos usuários, e cada usuário tem muitos jogos. Queremos recuperar todos os jogos pertencentes a um país por meio da tabela User.

Você definiria as tabelas da seguinte forma:

country
    id - integer
    name - string

user
    id - integer
    country_id - integer
    name - string

games
    id - integer
    user_id - integer
    title - string

Agora você deve definir o modelo do Eloquent para cada uma das tabelas:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    protected $fillable = ['name'];

    public function users()
    {
        return $this->hasMany(User::class);
    }

    public function games()
    {
        return $this->hasManyThrough(Games::class, User::class);
    }
}
<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $fillable = [article_id, 'name'];

    public function country()
    {
        return $this->belongsTo(Country::class);
    }

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

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Game extends Model
{
    protected $fillable = ['user_id', 'title'];

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

Agora podemos chamar o método games() do modelo Country para obter todos os jogos porque estabelecemos o relacionamento “Has Many Through” entre Country e Game por meio do modelo User.

<?php

$country = Country::find(159);
            
// Retrieve all games for the country
$games = $country->games;

Relacionamento muitos-para-muitos

O relacionamento de muitos-para-muitos é mais complicado. Um bom exemplo é um funcionário com várias funções. Uma função também pode ser atribuída a vários funcionários. Essa é a base do relacionamento de muitos-para-muitos.

Para isso, precisamos ter as tabelas employees, roles e role_employees.

Nossa estrutura de tabela do banco de dados terá a seguinte aparência:

employees
    id - integer
    name - string

roles 
    id - integer
    name - string

role_employees
    user_id - integer
    role_id - integer

Conhecendo a estrutura das tabelas de relacionamento, podemos facilmente definir nosso modelo Employee como belongToMany Role.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Employee extends Model
{
    public function roles() 
    {
        return $this- >belongsToMany(Role::class);
    }
}

Após definirmos isso, podemos acessar todas as funções de um funcionário e até mesmo filtrá-las:

$employee = Employee::find(1);
$employee->roles->forEach(function($role) { // });

// OR 

$employee = Employee::find(1)->roles()->orderBy('name')->where('name', 'admin')->get();

Como todos os outros métodos, podemos substituir as chaves estrangeiras e locais do método belongsToMany.

Para definir a relação inversa do belongsToMany, basta usar o mesmo método, mas agora no método secundário, com o principal como argumento.

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    public function employees() 
    {
        return $this->belongsToMany(Employee::class);
    }
}

Usos da tabela intermediária

Como você deve ter notado, quando usamos o relacionamento muitos-para-muitos, sempre devemos ter uma tabela intermediária. Nesse caso, estamos usando a tabela role_employees.

Por padrão, nossa tabela dinâmica conterá apenas o atributo id. Se quisermos outros atributos, teremos de especificá-los da seguinte forma:

return $this->belongsToMany(Employee::class)->withPivot("active", "created_at");

Se quisermos inserir um shortcode na tabela dinâmica para os registros de data e hora, podemos fazer isso:

return $this->belongsToMany(Employee::class)->withTimestamps();

Um truque que você deve saber é que podemos personalizar o nome “pivot” para qualquer coisa que se adapte melhor ao nosso aplicativo:

return $this->belongsToMany(Employee::class)->as('subscription')->withPivot("active", "created_by");

Filtrar os resultados de uma consulta eloquente é um conhecimento obrigatório para qualquer desenvolvedor que queira aprimorar seu jogo e otimizar seus aplicativos Laravel.

Portanto, o Laravel oferece um recurso fantástico de pivôs, que pode ser usado para filtrar os dados que você deseja coletar. Portanto, em vez de usar outros recursos, como transações do banco de dados, para obter nossos dados em partes, podemos filtrá-los com métodos úteis como wherePivot, wherePivotIn, wherePivotNotIn, wherePivotBetween, wherePivotNotBetween, wherePivotNull, wherePivotNotNull e podemos usá-los ao definir relacionamentos entre tabelas!

return $this->belongsToMany(Employee::class)->wherePivot('promoted', 1);
return $this->belongsToMany(Employee::class)->wherePivotIn('level', [1, 2]);
return $this->belongsToMany(Employee::class)->wherePivotNotIn('level', [2, 3]);
return $this->belongsToMany(Employee::class)->wherePivotBetween('posted_at', ['2023-01-01 00:00:00', '2023-01-02 00:00:00']);
return $this->belongsToMany(Employee::class)->wherePivotNull('expired_at');
return $this->belongsToMany(Employee::class)->wherePivotNotNull('posted_at');

Um último recurso incrível é que podemos ordenar por pivôs:

return $this->belongsToMany(Employee::class)
        ->where('promoted', true)
        ->orderByPivot('hired_at', 'desc');

Relacionamentos Polymorphic

A palavra Polymorphic vem do grego e significa “muitas formas” Assim, um modelo em nosso aplicativo pode assumir várias formas, o que significa que ele pode ter mais de uma associação. Imagine que estamos criando um aplicativo com blogs, vídeos, enquetes etc. Um usuário pode criar um comentário para qualquer um deles. Portanto, um modelo de comentário pode pertencer aos modelos de blogs, vídeos e enquetes.

Polimórfico de um-para-um

Esse tipo de relacionamento é semelhante a um relacionamento padrão de um-para-um. A única diferença é que o modelo secundário pode pertencer a mais de um tipo de modelo com uma única associação.

Por exemplo, um modelo Tenant e Landlord pode compartilhar uma relação polimórfica com um modelo WaterBill.

A estrutura da tabela pode ser a seguinte:

tenants
    id – integer
    name – string

landlords
    id – integer
    name – string

waterbills
    id – integer
    amount – double
    waterbillable_id
    waterbillable_type

Estamos usando waterbillable_id para a identificação do landlord ou tenant, enquanto waterbillable_type contém o nome da classe do modelo principal. A coluna type é usada pelo Eloquent para descobrir qual modelo principal você deve retornar.

A definição do modelo para esse relacionamento terá a seguinte aparência:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class WaterBill extends Model
{
    public function billable()
    {
        return $this->morphTo();
    }
}

class Tenant extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

class Landlord extends Model
{
    public function waterBill()    
    {
        return $this->morphOne(WaterBill::class, 'billable');
    }
}

Quando tivermos tudo isso pronto, poderemos acessar os dados dos modelos Landlord e Tenant:

<?php

$tenant = Tenant::find(1)->waterBill;
$landlord = Landlord::find(1)->waterBill;

Polimórfico de um-para-muitos

Isso é semelhante a uma relação regular de um para muitos, a única diferença fundamental é que o modelo secundário pode pertencer a mais de um tipo de modelo, usando uma única associação.

Em um aplicativo como o Facebook, os usuários podem comentar em publicações, vídeos, enquetes, live etc. Com uma associação polimórfica de um-para-muitos, podemos usar uma única tabela de comments para armazenar os comentários de todas as categorias que temos. A estrutura de nossas tabelas seria mais ou menos assim:

posts 
    id – integer
    title – string
    body – text

videos
    id – integer
    title – string
    url – string

polls
    id – integer
    title – string

comments 
    id – integer
    body – text
    commentable_id – integer
    commentable_type – string

O commentable_id é o ID do registro, e o commentable_type é o tipo de classe, para que o Eloquent saiba o que procurar. Quanto à estrutura do modelo, ela é muito semelhante à polimórfica um-para-muitos:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model 
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Poll extends Model
{
    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Live extends Model
{
    public function comments()
    {
        return $this->morphMany(Comments::class, 'commentable');
    }
}

Agora, para recuperar os comentários de uma live, podemos simplesmente chamar o método find com o id, e agora temos acesso à classe iterável de comentários:

<?php

use App\Models\Live;

$live = Live::find(1);

foreach ($live->comments as $comment) { }

// OR

Live::find(1)->comments()->each(function($comment) { // });
Live::find(1)->comments()->map(function($comment) { // });
Live::find(1)->comments()->filter(function($comment) { // });

// etc.

E se tivermos o comentário e quisermos descobrir a quem ele pertence, acessaremos o método commentable:

<?php

use App\Models\Comment;

$comment = Comment::find(10);
$commentable = $comment->commentable;

// commentable – type of Post, Video, Poll, Live

Polimórfico um-de-muitos

Em muitos aplicativos em escala, queremos uma maneira fácil de interagir com modelos e entre eles. Talvez queiramos a primeira ou a última publicação de um usuário, o que pode ser feito com uma combinação dos métodos morphOne e ofMany:

<?php

public function latestPost()
{
    return $this->morphOne(Post::class, 'postable')->latestOfMany();
}

public function oldestPost()
{
    return $this->morphOne(Post::class, 'postable')->oldestOfMany();
}

Os métodos latestOfMany e oldestOfMany estão recuperando o modelo mais recente ou mais antigo com base na chave primária do modelo, que era a condição para que ele pudesse ser classificado.

Em alguns casos, não queremos classificar pelo ID, talvez tenhamos alterado a data de publicação de algumas publicações e as queremos nessa ordem, não por seu ID.

Você pode fazer isso passando dois parâmetros para o método ofMany para ajudar nessa tarefa. O primeiro parâmetro é a key pela qual queremos filtrar, e o segundo é o sorting method:

<?php

public function latestPublishedPost()
{
    return $this->morphOne(Post::class, "postable")->ofMany("published_at", "max");
}

Com isso em mente, é possível criar relações mais avançadas para isso! Imagine que você tenha este cenário. Pedimos que você crie uma lista de todas as publicações atuais na ordem em que foram publicadas. O problema surge quando temos duas publicações com o mesmo valor published_at e quando as publicações estão programadas para serem publicadas no futuro.

Para fazer isso, podemos passar a ordem em que queremos que os filtros sejam aplicados ao método ofMany. Dessa forma, ordenamos por published_at e, se forem iguais, ordenamos por id. Em segundo lugar, podemos aplicar uma função de consulta ao método ofMany para excluir todos os artigos programados para publicação!

<?php

public function currentPosts()
{
    return $this->hasOne(Post::class)->ofMany([
        'published_at' => 'max',
        'id' => 'max',
    ], function ($query) {
        $query->where('published_at', '<', now());
    });
}

Polimórfico de muitos-para-muitos

O método polimórfico de muitos-para-muitos é um pouco mais complexo do que o normal. Uma situação comum é ter tags que se aplicam a mais ativos em seu aplicativo. Por exemplo, no TikTok, temos tags que podem ser aplicadas a vídeos, curtas, stories, etc.

O polimórfico muitos-para-muitos nos permite ter uma única tabela de tags associadas a vídeos, curtas e Stories.

A estrutura da tabela é simples:

videos
    id – integer
    description – string

stories 
    id – integer
    description – string

taggables 
    tag_id – integer
    taggable_id – integer
    taggable_type – string

Com as tabelas prontas, podemos criar o modelo e usar o método morphToMany. Esse método aceita o nome da classe do modelo e o “nome da relação”:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Video extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

E, com isso, podemos definir facilmente a relação inversa. Sabemos que, para cada modelo secundário, queremos chamar o método morphedByMany:

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function stories()
    {
        return $this->morphedByMany(Story::class, 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    } 
}

E agora, quando obtivermos uma tag, poderemos recuperar todos os vídeos e Stories relacionados a essa tag!

<?php
use App\Model\Tag;

$tag = Tag::find(10);
$posts = $tag->stories;
$videos = $tag->stories;

Otimize a velocidade do Eloquent

Ao trabalhar com o Eloquent ORM do Laravel, é essencial que você entenda como otimizar as consultas ao banco de dados e minimizar o tempo e a memória necessários para buscar dados. Uma maneira de fazer isso é implementar o cache em seu aplicativo.

O Laravel oferece um sistema de cache flexível que suporta vários backends, como Redis, Memcached e cache baseado em arquivo. Ao armazenar em cache os resultados das consultas do Eloquent, você pode reduzir o número de consultas ao banco de dados, tornando seu aplicativo mais rápido e valioso.

Além disso, você pode usar o construtor de consultas do Laravel para criar consultas complexas adicionais, otimizando ainda mais o desempenho do seu aplicativo.

Resumo

Concluindo, os relacionamentos Eloquent são um recurso poderoso do Laravel que permite aos desenvolvedores trabalhar facilmente com dados relacionados. De relacionamentos um-para-um a muitos-para-muitos, o Eloquent fornece uma sintaxe simples e intuitiva para você definir e consultar esses relacionamentos.

Como desenvolvedor do Laravel, o domínio dos relacionamentos do Eloquent pode melhorar muito seu fluxo de trabalho de desenvolvimento e tornar seu código mais eficiente e legível. Caso você esteja interessado em aprender mais sobre o Laravel, a Kinsta tem vários recursos disponíveis, incluindo um tutorial sobre como começar a usar o Laravel e um artigo sobre salários de desenvolvedores do Laravel.

A Kinsta oferece soluções de hospedagem gerenciada que facilitam a implantação e o gerenciamento de aplicativos Laravel.

Coman Cosmin

Cosmin Coman é um escritor de tecnologia e desenvolvedor com mais de 3 anos de experiência. Além de escrever para a Kinsta, ele ajudou em pesquisas em instalações de física nuclear e universidades. Com grande conhecimento técnico e integrado à comunidade, ele sempre vem com soluções inovadoras.