Sử dụng JSON Columns trong Eloquent: Cách tiếp cận lai SQL/NoSQL
Thiết kế cơ sở dữ liệu quan hệ truyền thống (Normalization) nói: "Nếu bạn có danh sách các item, hãy tạo một bảng riêng".
Nhưng đôi khi bạn chỉ muốn lưu trữ "Settings", "Metadata", hoặc các thuộc tính động mà không cần tạo 10 bảng mới.
Đây là lúc kiểu cột JSON xuất hiện.
Khi Nào Nên Dùng JSON Columns
JSON columns tỏa sáng trong các tình huống cụ thể:
- User preferences: Theme, thông báo, layout dashboard
- Product attributes: Size, màu sắc, chất liệu (thay đổi theo loại sản phẩm)
- API response caching: Lưu trữ tạm dữ liệu từ external API
- Feature flags: Lưu cấu hình A/B test
- Metadata: Thông tin bổ sung thay đổi theo từng record
Tránh JSON columns cho:
- Dữ liệu bạn query thường xuyên (dùng cột thông thường)
- Relationships (dùng foreign keys)
- Dữ liệu có cấu trúc nhất quán trên tất cả records
Migration
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->json('attributes'); // { "color": "red", "size": "XL" }
$table->json('meta')->nullable();
$table->timestamps();
});
Eloquent Casting
Cho Eloquent biết cần tự động chuyển đổi chuỗi JSON thành Array (hoặc Object).
protected $casts = [
'attributes' => 'array',
'meta' => 'object', // Sử dụng StdClass thay vì array
];
Bây giờ bạn có thể sử dụng nó như một PHP array bình thường:
$product->attributes['color'] = 'blue';
$product->save();
AsArrayObject Cast (Laravel 9+)
Để theo dõi mutation tốt hơn, sử dụng AsArrayObject:
use Illuminate\Database\Eloquent\Casts\AsArrayObject;
protected $casts = [
'attributes' => AsArrayObject::class,
];
Điều này tự động phát hiện các thay đổi lồng nhau:
$product->attributes['dimensions']['width'] = 100;
$product->save(); // Các thay đổi được theo dõi!
Truy vấn JSON
Laravel làm điều này cực kỳ dễ dàng sử dụng cú pháp ->.
// Tìm products có color là red
$products = Product::where('attributes->color', 'red')->get();
// Tìm size large
$products = Product::where('attributes->size', 'XL')->get();
// Keys lồng nhau
$users = User::where('meta->settings->notifications->email', true)->get();
// Kiểm tra key tồn tại (MySQL 5.7+)
$products = Product::whereNotNull('attributes->warranty')->get();
// Array chứa giá trị (PostgreSQL)
$products = Product::whereJsonContains('attributes->tags', 'featured')->get();
Cập nhật JSON một phần
Bạn có thể cập nhật một key đơn lẻ mà không ghi đè toàn bộ JSON blob.
// Cập nhật key cụ thể
$product->update(['attributes->color' => 'green']);
// Hoặc sử dụng query builder
Product::where('id', 1)
->update(['attributes->color' => 'green']);
Giá trị mặc định
Đặt giá trị mặc định cho JSON columns:
class Product extends Model
{
protected $attributes = [
'meta' => '{"views": 0, "featured": false}',
];
// Hoặc sử dụng attribute accessor
public function getAttributesAttribute($value)
{
return array_merge([
'color' => 'default',
'size' => 'medium',
], json_decode($value, true) ?? []);
}
}
Validation
Validate JSON data trong requests:
public function rules()
{
return [
'attributes' => 'required|array',
'attributes.color' => 'required|string|max:50',
'attributes.size' => 'required|in:S,M,L,XL,XXL',
'attributes.dimensions' => 'nullable|array',
'attributes.dimensions.width' => 'nullable|numeric|min:0',
'attributes.dimensions.height' => 'nullable|numeric|min:0',
];
}
Cảnh báo về hiệu năng
Truy vấn JSON chậm hơn các cột chuẩn. Nếu bạn truy vấn attributes->color trên mỗi lần tải trang, hãy tách nó ra thành một cột thực.
Tuy nhiên, bạn có thể đánh index cho JSON keys sử dụng "Generated Columns" trong MySQL 5.7+ / MariaDB.
// Migration
$table->string('color')
->virtualAs('JSON_UNQUOTE(JSON_EXTRACT(attributes, "$.color"))')
->index();
// Giờ điều này nhanh!
Product::where('color', 'red')->get();
Với PostgreSQL, sử dụng GIN indexes:
DB::statement('CREATE INDEX products_attributes_gin ON products USING GIN (attributes)');
JSON trong Factories
Tạo dữ liệu JSON thực tế trong factories:
class ProductFactory extends Factory
{
public function definition()
{
return [
'name' => $this->faker->productName(),
'attributes' => [
'color' => $this->faker->randomElement(['red', 'blue', 'green']),
'size' => $this->faker->randomElement(['S', 'M', 'L', 'XL']),
'weight' => $this->faker->randomFloat(2, 0.1, 10),
],
'meta' => [
'views' => $this->faker->numberBetween(0, 1000),
'featured' => $this->faker->boolean(20),
],
];
}
}
Best Practices
- Document cấu trúc JSON: Sử dụng PHPDoc hoặc các file schema riêng
- Validate kỹ lưỡng: Không bao giờ tin tưởng user input cho JSON fields
- Cân nhắc DTOs: Cast sang custom classes cho các cấu trúc JSON phức tạp
- Giám sát hiệu năng query: Theo dõi slow queries trên JSON columns
- Đặt giới hạn kích thước hợp lý: JSON columns có thể phình to bất ngờ
JSON columns mang đến sự linh hoạt của NoSQL trong khi vẫn giữ được độ tin cậy của SQL database. Sử dụng chúng một cách khôn ngoan, và chúng sẽ giúp bạn tránh schema bloat mà không hy sinh khả năng query.