عنوان:

‫بررسی رفتار نگاشت زمانی در SQLite و ارتقاء به دات‌نت ۱۰: تغییرات ساختاری در DateTimeOffset و راه‌کارهای مهاجرت داده


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۵ ۱۲:۰۵
آدرس: www.dntips.ir
چکیده: در فرآیند انتشار فریم‌ورک‌های مدرن دات‌نت، تغییرات ساختاری ناسازگار (Breaking Changes) معمولاً با خطاهای زمان کامپایل (Compile-time) یا تغییر در ساختار SQLهای تولیدی توسط ORM مشخص می‌شوند. با این حال، در NET 10. و در کتابخانه Microsoft.Data.Sqlite 10 (و لایه دیتابیس EF Core 10)، یک تغییر با درجه اهمیت بالا (High Impact) اعمال شده است که رفتاری کاملاً خاموش (Silent) دارد. این تغییر نحوه تفسیر و پارس رشته‌های زمانی فاقد آفست (Offset-less Timestamps) را بازتعریف می‌کند؛ رکوردهایی که پیش از این در دات‌نت ۹ به افست محلی ماشین (TimeZoneInfo.Local) نگاشت می‌شدند، در دات‌نت ۱۰ به صورت پیش‌فرض و غیرمشروط به عنوان زمان جهانی هماهنگ (UTC / +00:00) تفسیر می‌شوند. این مقاله به بررسی دقیق فنی، بازتولید سناریوی تغییر، تبعات داده‌ای پنهان، تغییرات تکمیلی روی REAL و DateTime.Kind و ارائه راهکارهای عملی جهت ممیزی و مهاجرت ایمن داده‌ها می‌پردازد.

مقدمه
پایگاه داده SQLite به دلیل سیستم تایپ پویا (Type Affinity)، فاقد نوع داده‌ای اختصاصی و بومی برای ذخیره‌سازی نوع‌های غنی زمانی مانند DateTimeOffset در دات‌نت است. به همین دلیل، پرووایدر Microsoft.Data.Sqlite و موتور EF Core داده‌های زمانی را معمولاً در ستون‌هایی از نوع متنی (TEXT) با فرمت ISO-8601 یا نوع عددی ممیز شناور (REAL مبتنی بر رکوردهای جولیان - Julian Day Numbers) ذخیره و بازیابی می‌کنند.
در نسخه‌های پیشین (شامل .NET 9 و قبل از آن)، در صورتیکه یک رشته زمانی بدون اطلاعات موقعیت زمانی (آفست صریح مثل Z یا 03:30+) در پایگاه داده وجود داشت، پرووایدر کلاینت هنگام تبدیل آن به DateTimeOffset، متغیر آفست را بر اساس منطقه زمانی سرور پردازش‌کننده (TimeZoneInfo.Local) تنظیم می‌کرد.
اگرچه این رفتار تلاش می‌کرد زمان محلی سیستم را لحاظ کند، اما به دلیل ناپایداری‌های شدید (تغییر آفست با جابه‌جایی سرور، تغییرات ساعت تابستانی یا DST و تفاوت رفتار در کلاسترهای ابری چندمنطقه‌ای)، تیم مهندسی دات‌نت تصمیم گرفت این رفتار را استانداردسازی کرده و به کنوانسیون رسمی خود SQLite نزدیک کند: تفسیر پیش‌فرض مقادیر بدون آفست بر مبنای UTC.

تحلیل فنی تغییرات
تغییرات اعمال‌شده در هسته Microsoft.Data.Sqlite 10 را می‌توان در سه محور اصلی دسته‌بندی کرد:


تغییر رفتار متدGetDateTimeOffsetدر ستون‌های متنی (TEXT)
اگر فیلدی در مدل به صورت DateTimeOffset تعریف شده باشد، نحوه خواندن آن از جدول تفاوت بنیادین پیدا کرده است:
  • در دات‌نت ۹: تابع پارسر مقدار محلی سرور را الصاق می‌کرد. برای مثال، در منطقه‌ای با تفاضل زمانی 01:00+ (مانند ساعت تابستانی بریتانیا/ایرلند IST)، خروجی نهایی به شکل 2026-08-14T20:00:00+01:00 حاصل می‌شد (معادل ساعت ۱۹:۰۰ زمان UTC).
  • در دات‌نت ۱۰: مقدار رشته بدون توجه به سرور یا کلاینت، به شکل 2026-08-14T20:00:00+00:00 پردازش می‌شود (دقیقاً ساعت ۲۰:۰۰ زمان UTC).

چالش اصلی: این تغییر سبب جابه‌جایی زمانی داده‌های تاریخی (Historical Data) می‌شود، بدون آنکه استثنایی (Exception) پرتاب گردد یا فرآیند اجرای برنامه متوقف شود.

ثبتDateTimeOffsetدر ستون‌های نوعREAL
در صورتی که نوع ستون بر روی اعداد اعشاری (REAL) پیکربندی شده باشد:
  • پیش از دات‌نت ۱۰: مقدار اعشاری روزهای جولیان بر اساس زمان محلی تولید و نوشته می‌شد.
  • در دات‌نت ۱۰: پرووایدر پیش از تبدیل به REAL و ذخیره‌سازی، داده را به صورت خودکار به معادل UTC تبدیل می‌کند.

تغییر وضعیتKindدر خواندنDateTimeبا متدGetDateTime
هنگامیکه از متد GetDateTime برای واکشی فیلدی استفاده می‌شود که در پایگاه داده دارای آفست صریح است:
  • در دات‌نت ۹: مقدار برگشتی به صورت DateTimeKind.Local علامت‌گذاری می‌شد.
  • در دات‌نت ۱۰: پرووایدر مقدار را با فلگ DateTimeKind.Utc برمی‌گرداند.

اگرچه لحظه زمانی یکسان است، اما اگر در کدهای تجاری (Business Logic) یا لایه‌های پایین‌دست، شرط‌ها یا انشعاباتی بر مبنای date.Kind == DateTimeKind.Local وجود داشته باشد، این تغییر مسیر اجرای برنامه را تحت تأثیر قرار خواهد داد.

بازتولید گام‌به‌گام سناریو
برای بازتولید و مشاهده مستقیم تفاوت رفتار در محیط توسعه:

مدل و دسترسی به پایگاه داده
فرض کنید موجودیتی به شکل زیر در پروژه تعریف شده است:
public class Event
{
    public int Id { get; set; }
    public string Description { get; set; } = string.Empty;
    public DateTimeOffset OccurredAt { get; set; }
}

درج رکورد فاقد آفست در پایگاه داده
یک رکورد بدون تعیین آفست، مستقیماً از طریق CLI در دیتابیس SQLite ثبت می‌شود:
sqlite3 migrationchecklist.db "INSERT INTO \"Events\" (\"Description\",\"OccurredAt\") VALUES ('OffsetlessTest','2026-08-14 20:00:00');"

اجرای تست مقایسه‌ای
با فراخوانی اندپوینت GET /events/1 خروجی‌های زیر حاصل می‌شوند:
  • پاسخ در .NET 9 (با فرض منطقه زمانی سرور روی UTC+1):
{
  "id": 1,
  "description": "OffsetlessTest",
  "occurredAt": "2026-08-14T20:00:00+01:00"
}
  • پاسخ در .NET 10 (روی همان فایل دیتابیس و بدون تغییر سیستم):
{
  "id": 1,
  "description": "OffsetlessTest",
  "occurredAt": "2026-08-14T20:00:00+00:00"
}

راهکارهای عملیاتی و استراتژی مهاجرت
جهت ایمن‌سازی فرآیند ارتقاء سیستم و جلوگیری از تحریف داده‌ها، پیمودن گام‌های زیر ضروری است:
گام اول: استفاده موقت از سوئیچ سازگاری (Compatibility Switch)
مایکروسافت برای پروژه‌های بزرگ سازوکاری قرار داده است تا توسعه‌دهندگان بتوانند رفتار پیشین را حین ممیزی حفظ کنند. این راهکار به عنوان یک راه‌حل موقت در فایل Program.cs یا پیش از مقداردهی دیتابیس فعال می‌شود:
AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);
همچنین می‌توان این سوئیچ را مستقیماً در فایل تنظیمات پروژه (runtimeconfig.json یا .csproj) اضافه کرد:
<ItemGroup>
  <RuntimeHostConfigurationOption Include="Microsoft.Data.Sqlite.Pre10TimeZoneHandling" Value="true" />
</ItemGroup>
هشدار: این کلید سازگاری صرفاً یک مسکّن موقت برای پیشبرد برنامه‌ریزی مهاجرت است و نباید به عنوان راه‌حل دائمی سیستم تلقی شود.

گام دوم: ممیزی داده‌های ذخیره‌شده
باید رکوردهایی که فاقد کاراکتر Z یا الگوهای آفست (+ یا -) هستند شناسایی شوند. کوئری نمونه زیر برای یافتن رشته‌های فاقد آفست کاربرد دارد:
SELECT Id, Description, OccurredAt 
FROM "Events"
WHERE OccurredAt NOT LIKE '%Z' 
  AND OccurredAt NOT LIKE '%+%' 
  AND OccurredAt NOT LIKE '%-%' 
  AND OccurredAt GLOB '[0-9][0-9][0-9][0-9]*';

گام سوم: استانداردسازی داده‌ها و اصلاح ساختار ذخیره‌سازی
پس از تعیین قصد اصلی داده (اینکه آیا مقادیر متعلق به زمان محلی سرور در آن تاریخ بوده‌اند یا به عنوان UTC ثبت شده بودند)، داده‌ها باید با فرمت کامل ISO-8601 بروزرسانی شوند.
در سمت لایه دسترسی به داده (EF Core)، برای پیشگیری قطعی از درج رشته‌های بدون آفست در آینده، توصیه می‌شود از Value Converter صریح استفاده شود:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // تضمین ثبت مقادیر با فرمت کامل استاندارد ISO-8601 شامل آفست زمانی
    modelBuilder.Entity<Event>()
        .Property(e => e.OccurredAt)
        .HasConversion(
            v => v.ToString("O"), // مثال: 2026-08-14T20:00:00.0000000+00:00
            v => DateTimeOffset.Parse(v, CultureInfo.InvariantCulture)
        );
}

جمع‌بندی
تغییر رخ‌داده در Microsoft.Data.Sqlite 10 هم‌راستاسازی ارزشمندی با اصول پایگاه‌های داده توزیع‌شده و استانداردهای جهانی زمانی است؛ زیرا وابستگی مقادیر ثبت‌شده به متغیرهای محیطی سیستم‌عامل سرور (TimeZoneInfo.Local) یک ضدالگو (Anti-pattern) به شمار می‌رود. با این حال، به دلیل ماهیت پنهان این تغییر در خواندن داده‌های تاریخی بدون آفست، تیم‌های توسعه‌دهنده دات‌نت پیش از مهاجرت نهایی به .NET 10 باید:
  • داده‌های خام فیلدهای زمانی در جداول SQLite را بازبینی و ممیزی کنند.
  • در صورت لزوم، مقادیر فاقد آفست را اصلاح کرده و فرمت ذخیره‌سازی را با استفاده از استاندارد Round-trip ("O") تثبیت نمایند.
  • در صورت نیاز به زمان بیشتر، از سوئیچ Pre10TimeZoneHandling صرفاً در طول دوره گذار بهره بگیرند.