Eloquent ORM: relacionamentos e queries

1. Introdução ao Eloquent ORM e Configuração Inicial

1.1. O que é Eloquent ORM e sua integração com Laravel

Eloquent é o ORM (Object-Relational Mapping) padrão do Laravel, que implementa o padrão Active Record. Ele permite interagir com bancos de dados relacionais utilizando objetos PHP, abstraindo a complexidade das queries SQL. Cada tabela do banco corresponde a um Model, e cada instância desse Model representa uma linha na tabela.

1.2. Definição de Models e convenções de nomenclatura

Por convenção, o nome da tabela é o plural snake_case do nome da classe Model. Por exemplo, User mapeia para users, Post para posts. A chave primária esperada é id (auto-increment), e as timestamps created_at e updated_at são gerenciadas automaticamente.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    // Se a tabela não seguir a convenção, defina explicitamente:
    protected $table = 'usuarios';

    // Se não usar timestamps:
    public $timestamps = false;
}

1.3. Configuração de conexão com banco de dados e migrations

A conexão é configurada no arquivo .env:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=meu_banco
DB_USERNAME=root
DB_PASSWORD=

Migrations são usadas para criar e modificar tabelas:

<?php
// database/migrations/xxxx_create_users_table.php
public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamps();
    });
}

2. Relacionamentos Básicos: Um para Um e Um para Muitos

2.1. Relacionamento hasOne / belongsTo: exemplo de perfil de usuário

Um usuário tem um perfil. No Model User:

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

No Model Profile:

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

Uso:

$user = User::find(1);
echo $user->profile->bio;

2.2. Relacionamento hasMany / belongsTo: posts de um usuário

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

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

2.3. Consultas encadeadas com relacionamentos

// Lazy loading (dispara queries separadas)
$user = User::find(1);
foreach ($user->posts as $post) {
    echo $post->title;
}

// Eager loading (uma única query com JOIN)
$users = User::with('posts')->get();

3. Relacionamentos Muitos para Muitos e Polimórficos

3.1. Relacionamento belongsToMany: papéis de usuários

Tabela pivô role_user com user_id e role_id:

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

// Model Role
public function users()
{
    return $this->belongsToMany(User::class);
}

3.2. Relacionamentos polimórficos

Comentários que podem pertencer a posts ou vídeos:

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

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

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

Estrutura da tabela comments:

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->text('body');
    $table->morphs('commentable'); // cria commentable_id e commentable_type
    $table->timestamps();
});

3.3. Manipulação de dados na tabela pivô

// Atributos extras na tabela pivô
public function roles()
{
    return $this->belongsToMany(Role::class)
                ->withPivot('level', 'assigned_at')
                ->withTimestamps();
}

// Sincronizar papéis
$user->roles()->sync([1 => ['level' => 5], 2 => ['level' => 3]]);

// Anexar sem remover existentes
$user->roles()->attach(3, ['level' => 2]);

// Desanexar
$user->roles()->detach(1);

4. Consultas Avançadas com Query Builder do Eloquent

4.1. Cláusulas where, orWhere, whereIn

$posts = Post::where('active', true)
             ->where(function ($query) {
                 $query->where('title', 'like', '%PHP%')
                       ->orWhere('body', 'like', '%PHP%');
             })
             ->whereIn('category_id', [1, 3, 5])
             ->get();

4.2. Ordenação, paginação e agrupamento

// Paginação
$users = User::orderBy('name')->paginate(15);

// Paginação simples (apenas next/previous)
$users = User::orderBy('name')->simplePaginate(15);

// Agrupamento com having
$categories = Post::select('category_id', DB::raw('count(*) as total'))
                  ->groupBy('category_id')
                  ->having('total', '>', 10)
                  ->get();

4.3. Subconsultas e joins manuais

// Subconsulta na seleção
$users = User::select('users.*')
    ->addSelect(['last_post_date' => Post::select('created_at')
        ->whereColumn('user_id', 'users.id')
        ->latest()
        ->limit(1)
    ])->get();

// Join manual
$posts = Post::join('users', 'posts.user_id', '=', 'users.id')
             ->where('users.active', true)
             ->select('posts.*', 'users.name as author')
             ->get();

5. Eager Loading, Lazy Loading e Otimização de Consultas

5.1. Diferença entre lazy loading e eager loading

// Lazy loading (N+1 problem)
$users = User::all();
foreach ($users as $user) {
    echo $user->posts->count(); // Executa 1 query para users + N queries para posts
}

// Eager loading (2 queries)
$users = User::with('posts')->get();

5.2. Eager loading condicional e aninhado

// Carregar apenas posts publicados
$users = User::with(['posts' => function ($query) {
    $query->where('published', true)->orderBy('created_at', 'desc');
}])->get();

// Relacionamentos aninhados
$users = User::with('posts.comments.author')->get();

5.3. Uso de load() e loadMissing()

$user = User::find(1);

// Carregar relacionamento após a consulta
$user->load('posts');

// Carregar apenas se ainda não foi carregado
$user->loadMissing('profile');

6. Agregações, Scopes e Mutators

6.1. Funções de agregação em relacionamentos

// Contar posts de cada usuário
$users = User::withCount('posts')->get();
foreach ($users as $user) {
    echo $user->posts_count;
}

// Agregações em relacionamentos
$users = User::withSum('posts', 'views')->get();
$users = User::withAvg('ratings', 'score')->get();

6.2. Escopos globais e locais

// Escopo local
public function scopeActive($query)
{
    return $query->where('active', true);
}

public function scopePopular($query, $minViews = 100)
{
    return $query->where('views', '>=', $minViews);
}

// Uso
$posts = Post::active()->popular(500)->get();

6.3. Mutators e Accessors

// Accessor (getter)
public function getFullNameAttribute()
{
    return "{$this->first_name} {$this->last_name}";
}

// Mutator (setter)
public function setPasswordAttribute($value)
{
    $this->attributes['password'] = bcrypt($value);
}

// Uso
echo $user->full_name; // Accessor
$user->password = 'secret'; // Mutator

7. Manipulação de Dados: Inserção, Atualização e Exclusão com Relacionamentos

7.1. Criação de registros relacionados

// Usando save()
$user = User::find(1);
$post = new Post(['title' => 'Novo Post', 'body' => 'Conteúdo']);
$user->posts()->save($post);

// Usando create()
$user->posts()->create(['title' => 'Outro Post', 'body' => 'Mais conteúdo']);

// Usando associate() para belongsTo
$post = Post::find(1);
$user = User::find(2);
$post->user()->associate($user);
$post->save();

7.2. Atualização em massa e upsert

// Atualização em massa
Post::where('category_id', 3)->update(['views' => 0]);

// Upsert (insert or update)
User::upsert([
    ['email' => 'user1@example.com', 'name' => 'User 1'],
    ['email' => 'user2@example.com', 'name' => 'User 2'],
], ['email'], ['name']);

7.3. Exclusão em cascata e soft deletes

// Migration com cascade
Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('user_id')
          ->constrained()
          ->onDelete('cascade');
});

// Soft deletes
use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use SoftDeletes;
    protected $dates = ['deleted_at'];
}

// Consultar com soft deletes
$posts = Post::withTrashed()->get();
$posts = Post::onlyTrashed()->get();

8. Boas Práticas, Performance e Debugging

8.1. Índices e profiling

// Migration com índices
Schema::table('posts', function (Blueprint $table) {
    $table->index('user_id');
    $table->index(['user_id', 'published']);
});

// Profiling com explain
$query = Post::where('user_id', 1)->toSql();
$explain = DB::select("EXPLAIN $query", [1]);

8.2. Cache de consultas

// Cache de queries
$users = Cache::remember('active_users', 3600, function () {
    return User::where('active', true)->get();
});

// Cache de relacionamentos
$user = User::find(1);
$posts = Cache::remember("user_{$user->id}_posts", 3600, function () use ($user) {
    return $user->posts()->with('comments')->get();
});

8.3. Ferramentas de debug

// Ver SQL gerado
$query = User::where('active', true);
dd($query->toSql(), $query->getBindings());

// Log de queries no AppServiceProvider
public function boot()
{
    DB::listen(function ($query) {
        logger($query->sql, $query->bindings);
    });
}

// Usando Laravel Debugbar (instalar com composer)
// composer require barryvdh/laravel-debugbar

Referências