كل المقالات

نسخ القراءة في ⁨Laravel⁩: لماذا يرى المستخدم بيانات قديمة بعد الحفظ

⁨Laravel⁩ · 4 دقيقة قراءة ·

إذا كان تطبيق ⁨Laravel⁩ لديك يرسل الكتابة إلى خادم قاعدة بيانات واحد والقراءة إلى نسخ متماثلة، فقد تصادف خطأ لا يظهر أبدًا على جهازك: يعدّل المستخدم عنوان مقال، ويحفظ، ويعود إلى الصفحة فيجد العنوان القديم. لم يفشل شيء. القراءة ذهبت إلى نسخة لم تنسخ التغيير بعد. والإصدار ⁨Laravel 13.38⁩ الصادر في أكتوبر 2026 يضيف أداتين لهذه الحالة بالضبط.

لماذا لا ينقذك ⁨sticky⁩

اتصال القراءة والكتابة في ⁨config/database.php⁩ له خيار اسمه ⁨sticky⁩. عند تفعيله، إذا كتب الطلب شيئًا ذهبت القراءات التي تليه إلى خادم الكتابة. المشكلة في النطاق: يستمر ذلك حتى نهاية الطلب نفسه فقط. والنموذج المعتاد طلب ⁨PUT⁩ يعيد التوجيه إلى ⁨GET⁩، أي طلبان. إعادة التوجيه تبدأ باتصال جديد، فتقرأ الصفحة التي تعرضها بعد الحفظ من نسخة متماثلة من جديد.

حفظ العنوان عرض الصفحة إعادة توجيه خادم الكتابة نسخة متأخرة تُنسخ لاحقًا يعود العنوان القديم
طلبان واتصالان. خيار ⁨sticky⁩ غطّى الأول وحده.

الحل للمستخدمين: ⁨PreventStaleDatabaseReads⁩

الوسيط الجديد يتذكّر الكتابة عبر الطلبات بوضع طابع زمني في الجلسة. بعد أن يكتب طلبٌ في الاتصال الافتراضي، تذهب قراءات هذا المستخدم إلى خادم الكتابة لمدة ثانيتين، ثم تعود إلى النسخ. أضفه إلى مجموعة ⁨web⁩ في ⁨bootstrap/app.php⁩:

use Illuminate\Foundation\Http\Middleware\PreventStaleDatabaseReads;

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        PreventStaleDatabaseReads::class,
    ]);
})

أو جرّبه على بعض المسارات أولًا، بنافذة أطول إن احتاجت نسخك ذلك:

Route::middleware(PreventStaleDatabaseReads::for(15))->group(function () {
    // routes where a user edits and then views
});

اختر مدة النافذة من مقاييس تأخر النسخ عندك، لا بالتخمين. النافذة الأقصر من التأخر الحقيقي تسمح بمرور البيانات القديمة. وهناك ثلاثة حدود تستحق المعرفة قبل الاعتماد عليه: يحتاج إلى جلسة، ويتتبع الاتصال الافتراضي وحده، ويغطي المستخدم الذي كتب فقط. مستخدم آخر يفتح الصفحة نفسها قد يحصل على النسخة الأقدم.

كتابة القراءة: خادم الكتابة القراءة: النسخ النافذة: ثانيتان افتراضيًا بعد النافذة جلسة مستخدم واحد
النافذة تتبع الجلسة، فتصمد بعد إعادة التوجيه التي لم يصمد لها ⁨sticky⁩.

الحل في الكود: ⁨DB::usingWritePdo()⁩

بعض الكود يحتاج بيانات حديثة أيًا كان من كتبها، مثل مهمة تعمل مباشرة بعد أن يحدّث ⁨webhook⁩ صفًا. الدالة ⁨onWriteConnection()⁩ موجودة من قبل، لكنها تنطبق على الاستعلام الذي تبدؤه فقط. حمّل علاقة من النتيجة وسيذهب استعلامها إلى نسخة. الدالة الجديدة ترسل كل قراءة داخل الدالة الممرَّرة إلى خادم الكتابة، ثم تعيد الإعداد السابق بعدها، حتى لو رمت الدالة استثناءً:

$drafts = DB::usingWritePdo(function () use ($email) {
    $user = User::query()->where('email', $email)->first();

    return $user?->posts()->whereNull('published_at')->get();
});

الكود الموجود أصلًا داخل ⁨DB::transaction()⁩ لا يحتاجها، فاستعلامات المعاملة تستخدم خادم الكتابة على أي حال.

عمّال الطوابير مشكلتهم معكوسة

في طلب الويب ينتهي ⁨sticky⁩ مع الاستجابة. أما عملية ⁨queue:work⁩ فتحتفظ بالاتصال نفسه بين المهام، فمهمة واحدة تكتب قد ترسل قراءات كل المهام التالية إلى خادم الكتابة حتى يُعاد تشغيل العامل. أضاف ⁨Laravel 13.36⁩ إعادة ضبط اختيارية: ⁨Worker::$flushStickyConnections = true⁩ داخل ⁨boot()⁩ في مزوّد خدمة. هي معطّلة افتراضيًا، فراجع المهام التي تقرأ بعد أن تكتب مهمة أخرى مباشرة قبل تفعيلها. ومع مشغّل الطوابير ⁨database⁩، أعطِ جدول المهام اتصالًا خاصًا عبر ⁨DB_QUEUE_CONNECTION⁩، وإلا فإن حجز المهمة يعيد رفع العلامة من جديد.

اختبره دون أن تخدع نفسك

كل سطر في سجل الاستعلامات يحمل المفتاح ⁨readWriteType⁩ بقيمة ⁨read⁩ أو ⁨write⁩، فيستطيع الاختبار أن يحفظ، ثم يطلب الصفحة، ثم يتحقق من أن أول استعلام كان ⁨write⁩. استخدم ⁨DatabaseMigrations⁩ أو ⁨DatabaseTruncation⁩ في هذا الاختبار. السمة ⁨RefreshDatabase⁩ تلفّ كل اختبار في معاملة، وكل استعلام في معاملة يستخدم خادم الكتابة، فينجح التحقق حتى بدون الوسيط.

خيار ⁨sticky⁩ يحمي طلبًا واحدًا. الكتابة ثم إعادة التوجيه ثم القراءة طلبان، فالقراءة القديمة بعد الحفظ تحتاج الوسيط المعتمد على الجلسة، والكود الذي يجب أن يكون حديثًا يحتاج ⁨usingWritePdo()⁩.

الخطوة التالية: إن كنت تشغّل نسخًا متماثلة، فابحث عن أسوأ تأخر سجّلته، وأضف ⁨PreventStaleDatabaseReads⁩ إلى المسارات التي يعدّل فيها المستخدم ثم يعرض، واجعل النافذة أطول من ذلك التأخر، واكتب اختبار ⁨readWriteType⁩ بدون ⁨RefreshDatabase⁩. الميزتان من إسهام ⁨Jack Bayliss⁩ في ⁨#62078⁩ و⁨#62099⁩. المصدر: مقال ⁨Laravel News⁩ عن منع القراءات القديمة بعد الكتابة.

ملاحظات القراء

  1. مفيش تعليقات لسه. الأول دايمًا الأجرأ.
بتبني حاجة زي كده؟ أقدر أساعدك. راسلني على واتساب