خداحافظی با دادهٔ کهنه از replica تو لاراول ۱۳.۳۸
خلاصهٔ کاملتر
بعضی اپهای لاراولی نوشتن رو به یه سرور دیتابیس و خوندن رو به یک یا چندتا replica (یعنی کپیای از دیتابیس که تغییرات سرور اصلی رو با کمی تأخیر میگیره) میفرستن. مشکل اینجاست که replica ممکنه عقب بیفته. مثلاً کاربر عنوان یه پست رو عوض میکنه، صفحه رو رفرش میکنه و هنوز عنوان قدیمی رو میبینه. Laravel 13.38 برای این مشکل middleware جدید PreventStaleDatabaseReads و متد usingWritePdo() رو آورده.
لاراول از قبل گزینهٔ sticky رو تو config/database.php داشت، ولی این فقط تا آخر همون request کار میکنه. فرمی که با PUT ارسال میشه و به یه GET ریدایرکت میکنه، در واقع دو request هست. پس GET دوباره از replica میخونه که شاید هنوز تغییر رو نگرفته باشه. middleware جدید رو میشه به گروه web تو bootstrap/app.php اضافه کرد یا فقط روی بعضی routeها گذاشت. پنجرهٔ زمانیش بهطور پیشفرض دو ثانیهست و با for() عوض میشه:
Route::middleware(PreventStaleDatabaseReads::for(15))->group(function () {
// ...
});به گفتهٔ نویسنده، دو ثانیه برای تیمی کافیه که تأخیر replicaش زیر یه ثانیهست. پیشنهادش اینه که پنجره رو برابر بیشترین تأخیری بذارید که تو متریکهای replica خودتون میبینید، چون پنجرهٔ کوتاهتر از تأخیر باز هم اجازه میده دادهٔ کهنه به کاربر برسه.
طرز کارش سه مرحله داره. قبل از controller، یه timestamp به اسم _database_modified_at رو از session میخونه و اگه تو پنجره باشه، useWriteConnectionWhenReading() رو صدا میزنه تا همهٔ selectها از سرور اصلی برن. بعد request عادی اجرا میشه. آخر سر با hasModifiedRecords() چک میکنه که آیا ردیفی insert، update یا delete شده، و اگه شده، زمان فعلی رو تو session ذخیره میکنه.
بعضی جاها هم مهم نیست کدوم کاربر تغییر داده و فقط دادهٔ تازه لازمه، مثل jobی که درست بعد از یه webhook اجرا میشه. onWriteConnection() فقط روی همون query اول اثر داره و مثلاً relationship posts() که یه query جدید میسازه، باز از replica میخونه. usingWritePdo() همهٔ خوندنهای داخل callback رو به سرور اصلی میفرسته و هر چی callback برگردونه رو برمیگردونه:
$drafts = DB::usingWritePdo(function () use ($email) {
$user = User::query()->where('email', $email)->first();
return $user?->posts()->whereNull('published_at')->get();
});برای تست هم هر ورودی query log یه کلید readWriteType با مقدار read یا write داره. نویسنده یادآوری میکنه که برای این تستها از DatabaseMigrations یا DatabaseTruncation استفاده کنید نه RefreshDatabase، چون اون هر تست رو تو transaction میپیچه و تو transaction همهٔ queryها از سرور اصلی میرن، پس تست حتی بدون middleware هم پاس میشه. این دو قابلیت رو Jack Bayliss تو PRهای #62078 و #62099 اضافه کرده.
نکات کلیدی:
- PreventStaleDatabaseReads از Laravel 13.38 اومده و پنجرهٔ پیشفرضش ۲ ثانیهست
- گزینهٔ قدیمی sticky فقط تو همون request کار میکنه و بعد از ریدایرکت اثری نداره
- middleware زمان آخرین نوشتن رو با کلید _database_modified_at تو session نگه میداره
- DB::usingWritePdo() برای یه بلوک کد همهٔ خوندنها رو به سرور اصلی میفرسته
- queryهای داخل DB::transaction() از قبل روی سرور اصلی اجرا میشن
- برای تست این رفتار RefreshDatabase نتیجهٔ غلط میده




