Đừng để chức năng "Đa ngôn ngữ" bóp nghẹt Database của bạn!
Lời mở đầu: Bẫy hiệu năng (Performance Trap)
Khi làm hệ thống Multi-language (Đa ngôn ngữ), chúng ta thường truyền một header X-Language: vi hoặc Accept-Language từ Frontend xuống Backend.
Nhiều anh em xử lý ở Middleware như thế này:
// ❌ BAD PRACTICE: Gọi DB trên mỗi request!
$langCode = $request->header('X-Language', 'en');
$language = Language::where('code', $langCode)->where('is_active', true)->first();
if ($language) {
App::setLocale($langCode);
}
Hậu quả: Bạn có 100 API. Khách hàng gọi API nào, hệ thống cũng bắt Database chạy câu query trên. Database của bạn đang phải chịu một áp lực khổng lồ cho một dữ liệu... gần như chẳng bao giờ thay đổi (một năm chắc sếp mới thêm 1 ngôn ngữ mới).
Hôm nay, chúng ta sẽ thiết kế lại luồng này chuẩn kỹ sư: Mọi thứ phải nằm trên RAM (Cache).
Bước 1: Xây dựng nền móng (Migration & Model)
Đầu tiên, tạo bảng languages để lưu trữ các ngôn ngữ hệ thống hỗ trợ.
php artisan make:model Language -m
File Migration:
// database/migrations/xxxx_create_languages_table.php
public function up(): void
{
Schema::create('languages', function (Blueprint $table) {
$table->id();
$table->string('code', 10)->unique(); // VD: vi, en, ja
$table->string('name', 50); // VD: Tiếng Việt, English
$table->string('icon')->nullable(); // Link ảnh cờ quốc gia
$table->boolean('is_active')->default(true); // Tắt/bật ngôn ngữ
$table->boolean('is_default')->default(false); // Ngôn ngữ mặc định
$table->timestamps();
});
}
File Model (Vũ khí bí mật nằm ở đây):
Chúng ta sẽ sử dụng Eloquent Events. Mỗi khi bảng languages có sự thay đổi (Thêm/Sửa/Xóa), ta sẽ tự động xóa Cache đi. Nhờ vậy, ta không bao giờ lo dữ liệu Cache bị cũ (Stale Data).
// app/Models/Language.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class Language extends Model
{
protected $fillable = ['code', 'name', 'icon', 'is_active', 'is_default'];
public const CACHE_KEY_ACTIVE = 'system_active_languages';
public const CACHE_KEY_DEFAULT = 'system_default_language';
// Hook vào các sự kiện của Model
protected static function booted()
{
$clearCache = function () {
Cache::forget(self::CACHE_KEY_ACTIVE);
Cache::forget(self::CACHE_KEY_DEFAULT);
};
// Khi có bất kỳ thay đổi nào, tự động quét sạch Cache
static::saved($clearCache);
static::deleted($clearCache);
}
}
Bước 2: Tạo Service lấy dữ liệu cực tốc độ
Thay vì query DB, ta tạo một Service chuyên lo việc lấy danh sách ngôn ngữ.
php artisan make:service LanguageService
// app/Services/LanguageService.php
namespace App\Services;
use App\Models\Language;
use Illuminate\Support\Facades\Cache;
class LanguageService
{
/**
* Lấy danh sách các mã ngôn ngữ đang Active (Cache vĩnh viễn cho đến khi bị xóa)
*/
public function getActiveLanguageCodes(): array
{
return Cache::rememberForever(Language::CACHE_KEY_ACTIVE, function () {
return Language::where('is_active', true)->pluck('code')->toArray();
});
}
/**
* Lấy mã ngôn ngữ mặc định
*/
public function getDefaultLanguageCode(): string
{
return Cache::rememberForever(Language::CACHE_KEY_DEFAULT, function () {
$default = Language::where('is_default', true)->first();
return $default ? $default->code : 'en'; // Fallback cứng về 'en' nếu chưa có set
});
}
}
Lợi ích: Với Cache::rememberForever, câu query DB chỉ chạy đúng 1 lần duy nhất. 1 triệu request tiếp theo sẽ được lấy thẳng từ RAM (Redis/Memcached/File) với tốc độ chưa tới 1ms.
Bước 3: Đánh chặn ở Middleware (Set Locale)
Bây giờ ta tạo Middleware để chặn mọi request, đọc header và set ngôn ngữ cho hệ thống.
php artisan make:middleware LocalizationMiddleware
// app/Http/Middleware/LocalizationMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use App\Services\LanguageService;
class LocalizationMiddleware
{
protected LanguageService $languageService;
public function __construct(LanguageService $languageService)
{
$this->languageService = $languageService;
}
public function handle(Request $request, Closure $next)
{
// 1. Đọc header do Client gửi lên
$requestedLang = $request->header('X-Language');
// 2. Lấy danh sách active từ RAM (Cache)
$activeLangs = $this->languageService->getActiveLanguageCodes();
// 3. Logic quyết định ngôn ngữ
if ($requestedLang && in_array($requestedLang, $activeLangs)) {
// Nếu Client gửi lên và ngôn ngữ đó đang được bật
App::setLocale($requestedLang);
} else {
// Nếu gửi láo, hoặc không gửi -> Trả về mặc định
App::setLocale($this->languageService->getDefaultLanguageCode());
}
return $next($request);
}
}
Đừng quên đăng ký Middleware này vào app/Http/Kernel.php (hoặc bootstrap/app.php đối với Laravel 11).
Bước 4: Controller quản lý (Cho màn hình Admin)
Cuối cùng, màn hình Admin cần các API để Thêm/Sửa/Xóa. Việc này giờ trở nên nhàn hạ, vì chúng ta không phải tự tay xóa Cache nữa (Model Event ở Bước 1 đã lo trọn gói).
// app/Http/Controllers/Api/Admin/LanguageController.php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\Language;
use Illuminate\Http\Request;
class LanguageController extends Controller
{
public function store(Request $request)
{
$request->validate([
'code' => 'required|string|unique:languages,code',
'name' => 'required|string',
]);
// Nếu tạo ngôn ngữ mặc định mới, phải reset cái cũ trước
if ($request->is_default) {
Language::where('is_default', true)->update(['is_default' => false]);
}
$language = Language::create($request->all());
// Cache tự động bị xóa nhờ sự kiện static::saved() trong Model
return response()->json(['message' => 'Tạo ngôn ngữ thành công', 'data' => $language]);
}
public function toggleActive(Language $language)
{
$language->update(['is_active' => !$language->is_active]);
return response()->json(['message' => 'Đã cập nhật trạng thái']);
}
}
Bước 5: Thử lửa với Postman - Khi kiến trúc lên tiếng!
Để chứng minh Middleware và Cache hoạt động hoàn hảo, chúng ta sẽ làm một bài test thực tế.
Chuẩn bị: Tạo một API và File dịch thuật mẫu
Trước khi mở Postman, hãy tạo nhanh 2 file ngôn ngữ và 1 Route để test.
- Tạo file
lang/en/messages.php:
return [
'welcome' => 'Welcome to our Enterprise System!',
];
- Tạo file
lang/vi/messages.php:
return [
'welcome' => 'Chào mừng bạn đến với hệ thống Doanh nghiệp!',
];
- Đăng ký một Route public trong
routes/api.php(nhớ áp dụng MiddlewareLocalizationMiddlewaređã tạo ở Bước 3):
Route::middleware([\App\Http\Middleware\LocalizationMiddleware::class])->group(function () {
Route::get('/hello', function () {
return response()->json([
'message' => __('messages.welcome') // Hàm __() sẽ tự động dịch theo Locale hiện tại
]);
});
});
// Route cho Admin (nhớ thêm middleware auth:api thực tế vào nhé)
Route::post('/admin/languages', [\App\Http\Controllers\Api\Admin\LanguageController::class, 'store']);
Kịch bản 1: Đóng vai Client (Người dùng cuối) gọi API
Khởi động server (php artisan serve) và mở Postman lên.
1. Gọi API không truyền ngôn ngữ (Hoặc truyền ngôn ngữ mặc định)
- Method:
GET - URL:
[http://127.0.0.1:8000/api/hello](http://127.0.0.1:8000/api/hello) - Headers:
Accept:application/json- (Không truyền X-Language)
Kết quả (Expected): Hệ thống sẽ lấy ngôn ngữ mặc định (Tiếng Anh).
{
"message": "Welcome to our Enterprise System!"
}
(Lúc này, hệ thống đã query DB 1 lần để lấy danh sách ngôn ngữ active và lưu tọt vào Cache).
2. Gọi API với ngôn ngữ Tiếng Việt
- Method:
GET - URL:
[http://127.0.0.1:8000/api/hello](http://127.0.0.1:8000/api/hello) - Headers:
Accept:application/jsonX-Language:vi
Kết quả (Expected):
{
"message": "Chào mừng bạn đến với hệ thống Doanh nghiệp!"
}
(Điều kỳ diệu ở đây là: Lần gọi này KHÔNG tốn một câu query DB nào để check xem 'vi' có tồn tại hay active không. Mọi thứ được đối chiếu siêu tốc qua RAM nhờ Cache).
Kịch bản 2: Đóng vai Admin - Test tính năng "Tự động dọn rác" (Auto Invalidation)
Giả sử công ty bạn vừa mở rộng sang thị trường Nhật Bản. Sếp yêu cầu thêm Tiếng Nhật (ja).
1. Thêm ngôn ngữ mới qua API Admin
- Method:
POST - URL:
[http://127.0.0.1:8000/api/admin/languages](http://127.0.0.1:8000/api/admin/languages) - Headers:
Accept:application/json - Body (raw - JSON):
{
"code": "ja",
"name": "Tiếng Nhật",
"is_active": true,
"is_default": false
}
Kết quả:
{
"message": "Tạo ngôn ngữ thành công",
"data": {
"code": "ja",
"name": "Tiếng Nhật",
"is_active": true,
"id": 3
}
}
(Ngay khoảnh khắc bạn nhấn Send và dữ liệu được lưu, event static::saved() trong Model Language đã âm thầm kích hoạt và Xóa sạch Cache cũ).
2. Client gọi API với Tiếng Nhật ngay lập tức Bây giờ, bạn quay lại Tab Postman của Client, đổi Header thành Tiếng Nhật.
- Headers:
Accept:application/jsonX-Language:ja
(Lưu ý: Bạn hãy tạo file lang/ja/messages.php và thêm chữ "企業システムへようこそ!" vào biến welcome nhé).
Kết quả: API trả về ngay lập tức Tiếng Nhật! Hệ thống tự động query DB lại ĐÚNG 1 LẦN để nạp danh sách ngôn ngữ mới (bao gồm ja) vào Cache, và các request sau lại tiếp tục lấy từ RAM.
Tóm lại
Phần test Postman này đã chứng minh 2 thứ giá trị nhất của kiến trúc chúng ta vừa xây:
- Dữ liệu động nhưng tốc độ tĩnh: Người dùng gọi API đổi ngôn ngữ linh hoạt nhưng hệ thống không bị thắt cổ chai ở Database.
- Không có độ trễ (Zero Downtime): Admin cập nhật dữ liệu là hệ thống tự động dọn Cache và nhận diện ngay lập tức, không cần Dev phải vào server chạy lệnh
php artisan cache:clearbằng tay một cách "phèn chúa".
Đó mới là cách một hệ thống Enterprise thực thụ vận hành!
All rights reserved