Laravel Table Names: When Singular Breaks Your Queries

📖 2 minutes read

You write a raw query in Laravel. It works in staging. You push to production, and suddenly: Table ‘database.products’ doesn’t exist.

Plot twist: The table is called product (singular). Welcome to the world of legacy databases.

Laravel’s Plural Assumption

Laravel’s Eloquent ORM follows a convention: table names are plural, model names are singular.

// Model: Product
// Expected table: products (plural)

class Product extends Model
{
    // Laravel auto-assumes 'products' table
}

This works great… until you inherit a database where someone used singular table names.

The Problem with Raw Queries

When you use Eloquent methods, Laravel uses $model->getTable(), which you can override:

class Product extends Model
{
    protected $table = 'product'; // Override to singular
}

// Eloquent queries work fine
Product::where('status', 'active')->get();

But raw queries don’t use getTable():

// ❌ Breaks if table is actually 'product' (singular)
DB::table('products')->where('status', 'active')->get();

// ❌ Also breaks
DB::select("SELECT * FROM products WHERE status = ?", ['active']);

The Fix: Use Model Table Names

Always reference the model’s table name dynamically:

// ✅ Uses the model's $table property
$table = (new Product)->getTable();

DB::table($table)->where('status', 'active')->get();

// ✅ Or inline
DB::table((new Product)->getTable())
    ->where('status', 'active')
    ->get();

For raw SQL strings:

$table = (new Product)->getTable();

DB::select("SELECT * FROM {$table} WHERE status = ?", ['active']);

Now if the table name changes (migration, refactor, database merge), you update it once in the model.

Why This Happens

Common scenarios where table names don’t match Laravel conventions:

  • Legacy databases: Built before Laravel, different naming standards
  • Multi-framework codebases: Database shared between Laravel and another app
  • Database naming policies: Company standards enforce singular table names
  • Third-party integrations: External systems dictate schema

Bonus: Check All Your Models

Want to see which models override table names?

grep -r "protected \$table" app/Models/

Or in Tinker:

collect(get_declared_classes())
    ->filter(fn($c) => is_subclass_of($c, Model::class))
    ->mapWithKeys(fn($c) => [$c => (new $c)->getTable()])
    ->toArray();

The Takeaway

Never hardcode table names in queries. Use getTable() to respect model configuration. When the table name changes, your queries won’t break.

Future-you debugging at 2 AM will thank you.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *