التعامل مع تواريخ الإصدارات القديمة
التعامل مع تواريخ الإصدارات القديمة
نادرًا ما تبدأ تطبيقات Java الحقيقية من صفحة بيضاء. ستصادر حتمًا واجهات برمجية ومكتبات وقواعد بيانات وطبقات تسلسل تتحدث لغة java.util.Date وjava.util.Calendar القديمة. صُمِّمت حزمة java.time مع توفير دوال جسر صريحة تتيح الهجرة التدريجية — لا حاجة لإعادة كتابة كل شيء دفعة واحدة.
يغطي هذا الدرس كل اتجاهات التحويل، والمزالق الخفية في دلالات المناطق الزمنية، والنمط العملي لتقرير متى تُحوِّل ومتى تترك النوع القديم كما هو.
خريطة سريعة للعالم القديم
قبل أن تبني الجسر بين العالمين تحتاج أن تعرف ما يقابل ماذا:
java.util.Date— طابع زمني بالملي ثانية منذ بداية حقبة Unix (1970-01-01T00:00:00Z). على الرغم من الاسم، ليس لديه مفهوم لتاريخ التقويم؛ فهو في الحقيقة غلاف حول قيمةlong.java.util.Calendar/GregorianCalendar— تمثيل قابل للتعديل يدرك المنطقة الزمنية ويجمع التاريخ والوقت وTimeZoneمعًا.java.sql.Dateوjava.sql.Timeوjava.sql.Timestamp— أنواع JDBC ترث منjava.util.Dateوتحمل دقة إضافية لكنها تقوم على نفس أساس الملي ثانية منذ الحقبة.
TimeZone فوقها. يجعل java.time هذا العقد صريحًا بدلًا من إخفائه.
من java.util.Date إلى java.time
حصل Date على دالة جسر وحيدة: toInstant(). بما أن Date ليس سوى عداد ملي ثواني، فإن الهدف الطبيعي هو Instant، ومنه تستطيع الإسقاط على أي نوع مدرك للمنطقة الزمنية أو محلي تحتاجه.
ZoneId.systemDefault() دون تفكير. المنطقة الافتراضية على الخادم تكون عادةً UTC أو منطقة نظام التشغيل وتتغير بين البيئات. كن صريحًا بشأن المنطقة المستهدفة وإلا أنتجت التحويلات بصمت تواريخ محلية مختلفة على أجهزة مختلفة.
من java.time إلى java.util.Date
الاتجاه المعاكس يستخدم المصنع الساكن Date.from(Instant). إذا كان مصدرك نوعًا مدركًا للمنطقة الزمنية حوّله إلى Instant أولًا.
LocalDate أو LocalDateTime يتطلب دائمًا منطقة زمنية. تفتقر هذه الأنواع عمدًا لمعلومات المنطقة الزمنية. إذا أهملت المنطقة ستُضطر لاختيارها — اجعل هذا الاختيار ظاهرًا في الكود لا مخفيًا داخل دالة مساعدة.
Calendar وGregorianCalendar
GregorianCalendar هي الفئة الملموسة التي تتعامل معها كل الوقت تقريبًا عند استخدام Calendar. لديها دالة تحويل مباشرة.
يحفظ الجسر الـTimeZone الأصلي تمامًا، فـCalendar في Asia/Tokyo يصبح ZonedDateTime بـAsia/Tokyo — دون إزاحة زمنية صامتة.
Calendar لتقويمات غير غريغورية: JapaneseImperialCalendar المستخدمة حين يكون locale الـJVM يابانيًا. في تلك الحالات استخرج الملي ثواني يدويًا: cal.getTimeInMillis()، ثم لفّها في Instant.ofEpochMilli(...).
أنواع JDBC: java.sql.Date و Time و Timestamp
أنواع JDBC هي أكثر مصادر التواريخ القديمة شيوعًا في تطبيقات الخلفية. لكل منها دالة جسر مباشرة toLocalXxx() — لاحظ أن هذه لا تمر بـInstant لأن تواريخ JDBC محلية صراحةً على مستوى البروتوكول.
Timestamp.valueOf(LocalDateTime) على Timestamp.from(Instant) عند الكتابة في عمود قاعدة بيانات من نوع DATETIME (لا TIMESTAMP WITH TIME ZONE). مسار valueOf يحافظ على حقول التقويم الحرفية؛ مسار from يطبّق المنطقة الافتراضية للـJVM مما قد يُزيح القيم بصمت في MySQL وما شابهها.
نمط هجرة عملي
حين تُورَث قاعدة كود كبيرة لا تستطيع دائمًا إعادة كتابة كل طبقة دفعة واحدة. إليك استراتيجية تدريجية آمنة:
- حوّل عند الحدود. دع فئات النموذج الداخلية وكائنات نقل البيانات تستخدم
java.time. حوّل من/إلى الأنواع القديمة فقط عند حدود الإدخال/الإخراج — حيث تستدعي مكتبة قديمة أو تقرأ من JDBC. - اكتب دالة محوّل مخصصة. مركز منطق التحويل. عندما تُحدّث المكتبة القديمة لاحقًا تغيّر مكانًا واحدًا فقط.
- استخدم
@SuppressWarnings("deprecated")بتحفّظ. ابقِ كود التحويل القديم معزولًا ليسهل حذفه حين تختفي التبعية القديمة.
الخلاصة
لكل نوع قديم جسر نظيف إلى java.time:
Date.toInstant()/Date.from(Instant)— البوابة العالمية.GregorianCalendar.toZonedDateTime()/GregorianCalendar.from(ZonedDateTime)— يحفظ المنطقة الزمنية الأصلية.java.sql.Date.toLocalDate()وTime.toLocalTime()وTimestamp.toLocalDateTime()— مسارات JDBC بلا منطقة زمنية.
القاعدة الذهبية: كن صريحًا دائمًا بشأن المناطق الزمنية أثناء التحويل. معظم الأخطاء التي اعترت الواجهة القديمة نشأت من افتراضات ضمنية للمنطقة الزمنية — يُلزمك java.time بالتصريح بهذه الافتراضات في الكود.