You just defined a new Eloquent relationship. Now you need to write the migration. What columns should you create? What should they be named? What foreign keys do you need?
Stop guessing. Use toRawSql() to see exactly what Eloquent expects before you write a single line of migration code.
The Problem: Eloquent’s Naming Conventions
Laravel has conventions for relationship columns:
hasManyexpectsparent_idin the child tablemorphToManyexpectstaggable_idandtaggable_typehasManyThroughexpects specific columns in both intermediate and target tables
If you get any of these wrong, your queries fail at runtime. The fix: inspect the raw SQL before you migrate.
Using toRawSql() in Tinker
Define your relationship in the model:
// app/Models/Order.php
public function items()
{
return $this->hasMany(Item::class);
}
Before writing the migration, open php artisan tinker and run:
Order::has('items')->toRawSql();
Output:
select * from `orders` where exists (
select * from `items`
where `orders`.`id` = `items`.`order_id`
)
Boom. Eloquent expects an order_id column in the items table. Now you know what to create:
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained();
$table->string('name');
$table->timestamps();
});
Example: Polymorphic Relationships
Let’s say you’re building a tagging system. Products can have tags:
// app/Models/Product.php
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
What columns does this need? Check Tinker:
Product::has('tags')->toRawSql();
Output:
select * from `products` where exists (
select * from `tags`
inner join `taggables` on `tags`.`id` = `taggables`.`tag_id`
where `products`.`id` = `taggables`.`taggable_id`
and `taggables`.`taggable_type` = 'App\\Models\\Product'
)
Now you know:
- Pivot table must be named
taggables - It needs
tag_id,taggable_id, andtaggable_typecolumns - The
taggable_typecolumn will store the full model class name
Write the migration:
Schema::create('taggables', function (Blueprint $table) {
$table->id();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable'); // Creates taggable_id and taggable_type
$table->timestamps();
$table->unique(['tag_id', 'taggable_id', 'taggable_type']);
});
Debugging Existing Relationships
If a relationship query is failing, toRawSql() shows you what Eloquent is actually looking for:
// This query is failing. Why?
$orders = Order::with('customer')->get();
// Check the raw SQL
Order::with('customer')->toRawSql();
Output shows it’s looking for customer_id in the orders table, but your column is named user_id. Fix it in the relationship definition:
public function customer()
{
return $this->belongsTo(User::class, 'user_id');
}
Advanced: Nested Relationships
For complex queries with nested eager loading, toRawSql() reveals the entire join chain:
Order::with('items.product')->toRawSql();
This shows you all the foreign keys Eloquent expects across the entire relationship chain. Invaluable when working with hasManyThrough or deeply nested relationships.
Pro Tips
- Use
toRawSql()beforemigrate: Define relationships first, inspect SQL, then write migrations. Catches naming mismatches immediately. - Works with any query builder method:
whereHas,with,has,withCountβif it generates SQL,toRawSql()shows it. - Copy-paste into a database client: Run the raw SQL directly to test if the schema matches your expectations.
- Laravel 9+: In earlier versions, use
toSql()instead (shows SQL with?placeholders).
Bonus: Debugging Performance Issues
toRawSql() also helps spot N+1 queries and missing indexes:
// Are we eager loading properly?
Product::with('category', 'tags')->toRawSql();
// Check if the query uses the right index
Product::where('status', 'active')->orderBy('created_at', 'desc')->toRawSql();
Paste the SQL into EXPLAIN to see if your indexes are being used.
Stop Guessing, Start Inspecting
Eloquent’s conventions are predictable, but when you’re dealing with polymorphic relationships, custom foreign keys, or deep nesting, toRawSql() removes all guesswork. Define the relationship, inspect the SQL, write the migration. No runtime surprises.
Leave a Reply