بيئة العمل: PHP 8.2.13 على Ubuntu 22.04، حاويات Docker، ووقاعدة بيانات MySQL 8.0 فيها جدول orders بحوالي 4.2 مليون صف. ظهرت مشكلة بعد ترحيل جزء من الخدمات: تقرير يومي كان يعطي أرقامًا مختلفة بين خادم الإنتاج وخادم الاختبار، رغم أن نفس الكود ونفس البيانات. المشكلة لم تكن في SQL، بل في strtotime() وDateTime عند بناء حدود اليوم.
السبب الجذري: date.timezone في php.ini كان UTC في الإنتاج وAfrica/Cairo في الاختبار، وهو ما لم يكن أحد قد انتبه إليه. هذا لا يكسر الكود، بل يجعل نطاق اليوم مختلفًا بساعتين أو ثلاث. ولأن الاستعلام يستخدم BETWEEN على عمود created_at من نوع DATETIME، فإن الفارق الزمني يغيّر الصفوف المشمولة.
ما فعلته أولًا هو تثبيت المنطقة الزمنية في نقطة واحدة بدل الاعتماد على الإعداد العام. في bootstrap أو في ملف الإعداد الخاص بالتطبيق:
date_default_timezone_set('UTC');
ثم استبدلت حساب حدود اليوم ببناء صريح باستخدام DateTimeImmutable مع منطقة زمنية محددة، وتمرير الحدود كسلاسل بصيغة Y-m-d H:i:s إلى الاستعلام:
$tz = new DateTimeZone('UTC');
$from = new DateTimeImmutable('today', $tz);
$to = $from->modify('+1 day');
$stmt = $pdo->prepare(
'SELECT COUNT(*) FROM orders WHERE created_at >= ? AND created_at < ?'
);
$stmt->execute([
$from->format('Y-m-d H:i:s'),
$to->format('Y-m-d H:i:s'),
]);
لاحظت أن استخدام >= و< بدل BETWEEN يمنع تداخل الحدود عند منتصف الليل، وهو خطأ شائع عندما يكون العمود DATETIME وليس TIMESTAMP. الفرق يظهر فقط عند الصفوف التي تساوي to بالضبط، لكنه يصبح مهمًا عند التجميع اليومي على ملايين الصفوف.
السبب في اختيار UTC هنا هو أن قاعدة البيانات والتطبيق يعملان على نفس الأساس، وأي عرض للمستخدم يتم تحويله في طبقة العرض. هذا له ثمن: كل تقرير يحتاج تحويلًا صريحًا إلى المنطقة الزمنية المحلية، وإلا سيقرأ المستخدم في القاهرة أرقامًا تبدو متأخرة بثلاث ساعات. كما أن أي بيانات قديمة أُدخلت بمنطقة زمنية مختلفة ستحتاج تدقيقًا يدويًا، لأن DATETIME لا يحفظ المنطقة الزمنية أصلًا.
النقطة التي ينكسر عندها هذا الحل: إذا كان العمود TIMESTAMP فإن MySQL يحوّل القيمة تلقائيًا حسب time_zone للجلسة، وهنا يصبح ضبط date_default_timezone_set غير كافٍ. أيضًا إذا كانت بعض الخدمات تكتب بـNOW() والأخرى بـdate('Y-m-d H:i:s') فستحصل على مصدرين للوقت قد يختلفان عند تغيير إعداد الخادم. جربت مرة تشغيل الاختبارات مع TZ=Africa/Cairo فمرّت، ثم فشلت في CI لأن الصورة كانت UTC؛ الحل كان تثبيت المنطقة الزمنية في docker-compose.yml وفي phpunit.xml معًا.
السؤال الذي أطرحه: كيف تتعاملون مع الأعمدة القديمة من نوع DATETIME التي لا تحمل منطقة زمنية، هل تعيدون كتابتها إلى UTC أم تتركونها وتضبطون التحويل في طبقة الاستعلام؟


0 条评论
还没有评论,写下第一条。