Database Constraints in Laravel: Beyond FormRequest Validation
"Why application validation is insufficient: enforce native database constraints in Laravel migrations for absolute data integrity."
Database Constraints in Laravel: Beyond FormRequest Validation
Relying exclusively on application-level validation (FormRequest or $request->validate()) leaves systems vulnerable to race conditions under high concurrency and bypasses direct SQL executions.
1. Race Conditions in the unique Rule
Laravel's unique validation rule executes SELECT COUNT(*) WHERE email = ? before inserting.
// Request A and Request B hit the server concurrently within milliseconds
$request->validate(['email' => 'required|unique:users,email']);
- Request A runs
SELECT COUNT(*), returns 0 (Passes). - Request B runs
SELECT COUNT(*), returns 0 (Passes). - Request A executes
INSERT INTO users (email) VALUES ('test@mail.com'). - Request B executes
INSERT INTO users (email) VALUES ('test@mail.com').
Result: Duplicate records exist if there is no unique index at the database engine level.
Fix: Migration Unique Constraint
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('email')->unique(); // Native unique index
$table->timestamps();
});
2. Composite Unique Constraints for Paired Columns
Example: Restricting users to exactly one vote per article.
Schema::create('votes', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->timestamps();
// Enforce unique pair
$table->unique(['user_id', 'post_id']);
});
3. Check Constraints for Numeric Integrity
Instead of relying solely on validation rules like min:0, enforce Check Constraints in migrations (supported in MySQL 8.0+, PostgreSQL, and SQLite).
Schema::create('wallets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->bigInteger('balance')->default(0);
$table->timestamps();
// Prevent negative balance directly at DB storage engine
$table->check('balance >= 0');
});
4. Foreign Key Cascades
Use native cascading foreign keys to ensure referential integrity during entity deletions:
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
Summary
- Application validation (
FormRequest) provides clear UX error messages. - Database Constraints (
UNIQUE,CHECK,FOREIGN KEY) guarantee data integrity against concurrent race conditions and application-level bugs.
About the Author
huud
@huud
Systems architect and software engineer building high-performance distributed platforms.