0

ActiveRecord pattern — what it is and trade-offs

Intermediate5 min read·lv-12-001
interviewsqlcompare

Concept

Eloquent ORM implements the Active Record pattern. In Active Record, a class represents a database table and each instance represents a row. The model knows how to read and write itself to the database — it merges business logic and persistence in one object.

Trade-offs of Active Record:

  • Pro: Simple, intuitive. $user->save() is immediately understandable. Great for CRUD operations.
  • Pro: Lazy loading — relationships load on demand without explicit queries.
  • Con: Model objects carry database concerns — tight coupling between domain logic and persistence.
  • Con: Harder to test in isolation (requires a database or mocking the ORM).
  • Con: N+1 queries — easy to trigger accidentally (solved with eager loading).
  • Con: God objects — models can grow to include too many responsibilities.

Alternative: Data Mapper (Doctrine): The model knows nothing about persistence. A separate EntityManager handles queries and mapping. More complex, better separation, preferred for complex domains.

When Active Record (Eloquent) excels: Standard web apps, admin panels, CRUD-heavy applications. The simplicity boost usually outweighs the architectural downsides for most Laravel projects.

The Model class: Illuminate\Database\Eloquent\Model. Provides: attribute access via $model->name, query building via Model::where(), relationships, events, observers, soft deletes, scopes, casts, serialization.

Code Example

php
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

// Active Record — the model IS the persistence mechanism
class User extends Model
{
    // Table name: users (inferred from class name, pluralized)
    // Primary key: id (default)
    // Timestamps: created_at, updated_at (default)

    protected $fillable = ['name', 'email', 'password'];
    protected $hidden = ['password', 'remember_token'];
    protected $casts = ['email_verified_at' => 'datetime'];
}

// CRUD in Active Record style
// Create
$user = User::create(['name' => 'Alice', 'email' => 'alice@example.com', 'password' => bcrypt('secret')]);

// Read
$user = User::find(1);               // null if not found
$user = User::findOrFail(1);         // throws ModelNotFoundException
$users = User::where('active', 1)->get();

// Update
$user->name = 'Alice Smith';
$user->save();
// Or:
User::where('id', 1)->update(['name' => 'Alice Smith']);

// Delete
$user->delete();
User::destroy(1);
User::destroy([1, 2, 3]);

// The model carries both state and behavior
class Order extends Model
{
    public function calculateTotal(): float
    {
        return $this->items->sum(fn($item) => $item->price * $item->quantity);
    }

    public function isCancellable(): bool
    {
        return $this->status === 'pending' && $this->created_at->diffInHours() < 24;
    }

    public function cancel(): void
    {
        if (!$this->isCancellable()) {
            throw new OrderCannotBeCancelledException($this->id);
        }
        $this->update(['status' => 'cancelled']);
        event(new OrderCancelled($this));
    }
}