ActiveRecord pattern — what it is and trade-offs
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
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));
}
}