laraveleloquentquery-builderbase-de-datos

Eloquent vs Query Builder en Laravel — Cuándo usar cada uno

Esta es una pregunta que surge constantemente en proyectos Laravel: ¿uso Eloquent o el Query Builder? La respuesta, como casi siempre en desarrollo de software, es “depende”. Pero vamos a ver exactamente de qué depende y con qué criterios tomar la decisión.

¿Qué es cada uno?

Eloquent ORM

Eloquent es el ORM (Object-Relational Mapper) de Laravel. Representa cada tabla de la base de datos como un modelo PHP. Cuando consultas con Eloquent, obtienes objetos con métodos, eventos, relaciones y más:

// Eloquent: trabajas con objetos Model
$posts = Post::where('published', true)
             ->with('user', 'tags')
             ->orderByDesc('created_at')
             ->paginate(15);

foreach ($posts as $post) {
    echo $post->user->name;  // Relación cargada automáticamente
    echo $post->created_at->diffForHumans();  // Carbon object
}

Query Builder

El Query Builder es una capa por encima de PDO que te permite construir queries SQL de forma fluida sin escribir SQL crudo:

// Query Builder: trabajas con colecciones de stdClass o arrays
$posts = DB::table('posts')
           ->where('published', 1)
           ->join('users', 'posts.user_id', '=', 'users.id')
           ->select('posts.*', 'users.name as author_name')
           ->orderByDesc('posts.created_at')
           ->paginate(15);

foreach ($posts as $post) {
    echo $post->author_name;  // stdClass property
    echo $post->created_at;   // String, no Carbon
}

Las diferencias clave

1. Lo que devuelven

// Eloquent devuelve instancias del Model
$user = User::find(1);
// $user es una instancia de App\Models\User
// Tiene: $user->posts(), $user->isAdmin(), $user->getFullNameAttribute(), etc.

// Query Builder devuelve stdClass o arrays
$user = DB::table('users')->find(1);
// $user es stdClass
// Solo tiene las columnas de la tabla, nada más

2. Relaciones y eager loading

Eloquent tiene un sistema de relaciones sofisticado:

// Eloquent: relaciones definidas en el modelo
class Post extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
    
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class);
    }
}

// Eager loading para evitar N+1
$posts = Post::with(['user:id,name', 'tags'])->get();

Con Query Builder, debes hacer los JOINs manualmente:

// Query Builder: JOINs explícitos
$posts = DB::table('posts')
           ->join('users', 'posts.user_id', '=', 'users.id')
           ->leftJoin('post_tag', 'posts.id', '=', 'post_tag.post_id')
           ->leftJoin('tags', 'post_tag.tag_id', '=', 'tags.id')
           ->select('posts.*', 'users.name', 'tags.name as tag_name')
           ->get();
// Obtienes los datos desnormalizados: un post por cada tag

3. Mutadores, accesores y casting

// Eloquent: casting automático y accesores
class User extends Model
{
    protected $casts = [
        'email_verified_at' => 'datetime',
        'settings'          => 'array',     // JSON → array automáticamente
        'is_admin'          => 'boolean',
    ];

    // Accesores
    public function getFullNameAttribute(): string
    {
        return "{$this->first_name} {$this->last_name}";
    }
}

$user = User::find(1);
echo $user->full_name;      // Usa el accesor
echo $user->settings['theme'];  // Array, no string JSON
echo $user->created_at->format('d/m/Y');  // Carbon object

Con Query Builder, recibes los valores exactos de la BD: JSON como string, fechas como string, booleanos como 0/1.

4. Eventos y observers

Eloquent tiene un sistema de eventos que se disparan automáticamente:

// Se dispara cuando creas un usuario con Eloquent
class UserObserver
{
    public function created(User $user): void
    {
        // Enviar email de bienvenida
        Mail::to($user)->send(new WelcomeMail($user));
    }
}

User::create(['email' => '...']); // Dispara el evento 'created'

// Query Builder BYPASEA todos los eventos:
DB::table('users')->insert([...]); // NO dispara eventos, NO usa Observers

Esto es muy importante saberlo: si usas DB::table() para insertar, los observers de Eloquent no se ejecutarán.

Rendimiento: ¿cuánto importa?

La pregunta más común es “¿no es Eloquent más lento?”. Técnicamente sí, hay overhead. Pero en la práctica:

// Para consultas normales (decenas o cientos de registros):
// La diferencia es imperceptible para el usuario

// Para reportes con millones de registros:
// El Query Builder puede ser 2-5x más rápido

// Ejemplo: generar un CSV de 1 millón de registros
// Eloquent: puede agotar la memoria y tardar minutos
// Query Builder: mucho más eficiente
$users = DB::table('users')
           ->select('id', 'name', 'email')
           ->orderBy('id')
           ->cursor(); // Generator, muy eficiente en memoria

foreach ($users as $user) {
    fputcsv($file, [$user->id, $user->name, $user->email]);
}

El método toSql() para debugging

Muy útil para ver qué SQL genera tu query:

// Ver el SQL de Eloquent
$sql = Post::where('published', true)
           ->with('user')
           ->toSql();
// SELECT * FROM `posts` WHERE `published` = ?

// Ver el SQL con los bindings reales
$query = Post::where('published', true)->where('user_id', 5);
dump($query->toSql());
dump($query->getBindings());

// Alternativa: log de todas las queries
DB::listen(function ($query) {
    Log::debug($query->sql, $query->bindings);
});

Cuándo usar cada uno

Usa Eloquent cuando:

// Trabajas con lógica de negocio compleja
// Necesitas relaciones (belongsTo, hasMany, etc.)
// Tienes eventos/observers que deben ejecutarse
// Necesitas casting de tipos (JSON, fechas, etc.)
// Construyes funcionalidades CRUD estándar

$order = Order::create([
    'user_id' => $user->id,
    'total'   => $cart->total(),
    'status'  => 'pending',
]);
// Dispara el evento 'created', ejecuta observers,
// hace casting automático de 'total' a float

Usa Query Builder cuando:

// Reportes con grandes volúmenes de datos
// Queries complejas con múltiples JOINs y agregaciones
// Operaciones masivas (bulk insert, bulk update)
// Necesitas máximo rendimiento en un proceso batch

// Reporte de ventas por categoría (query compleja)
$report = DB::table('orders')
            ->join('order_items', 'orders.id', '=', 'order_items.order_id')
            ->join('products', 'order_items.product_id', '=', 'products.id')
            ->join('categories', 'products.category_id', '=', 'categories.id')
            ->whereBetween('orders.created_at', [$startDate, $endDate])
            ->where('orders.status', 'completed')
            ->groupBy('categories.id', 'categories.name')
            ->select(
                'categories.name',
                DB::raw('COUNT(DISTINCT orders.id) as total_orders'),
                DB::raw('SUM(order_items.quantity) as total_units'),
                DB::raw('SUM(order_items.price * order_items.quantity) as revenue')
            )
            ->orderByDesc('revenue')
            ->get();

Bulk operations

// Bulk insert con Query Builder (mucho más rápido que Eloquent)
$data = array_map(fn($i) => [
    'name'       => "Product {$i}",
    'price'      => rand(10, 100),
    'created_at' => now(),
    'updated_at' => now(),
], range(1, 10000));

// Eloquent: 10,000 inserts individuales (lento, dispara eventos x10,000)
// foreach ($data as $item) Product::create($item); // NO hagas esto

// Query Builder: una sola query SQL con muchos valores (rápido)
DB::table('products')->insert($data);

// O en chunks para no sobrecargar
foreach (array_chunk($data, 500) as $chunk) {
    DB::table('products')->insert($chunk);
}

Se pueden combinar

Eloquent y Query Builder no son mutuamente excluyentes. Puedes usar Eloquent para consultas normales y el Query Builder para operaciones específicas:

// Eloquent para operaciones normales
$user = User::findOrFail($id);
$user->posts()->create(['title' => 'Nuevo post']);

// Query Builder para una operación masiva en el mismo request
DB::table('analytics_events')
   ->where('session_id', session()->getId())
   ->update(['user_id' => $user->id]);

Conclusión

Usa Eloquent como tu herramienta principal para el 90% de las operaciones. Es más expresivo, integra con el ecosistema Laravel (eventos, observers, casting) y hace tu código más legible. Cambia al Query Builder cuando necesites rendimiento en operaciones masivas o cuando construyas queries analíticas complejas con muchos JOINs y agregaciones. No es una elección de uno u otro para toda la aplicación, sino la herramienta correcta para cada contexto.