عنوان:

‫پشتیبان‌گیری آنلاین از پایگاه داده SQLite در محیط .NET با استفاده از VACUUM INTO


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۱۱/۰۹ ۱۰:۵۰
آدرس: www.dntips.ir
در کاربردهای سرورمحور که پایگاه داده SQLite به عنوان یک راه‌حل سبک و کارآمد مورد استفاده قرار می‌گیرد، پشتیبان‌گیری (Backup) از داده‌ها بدون اختلال در سرویس‌دهی یک چالش کلیدی است. تصور کنید سروری که به طور مداوم درخواست‌های خواندن و نوشتن را مدیریت می‌کند؛ در چنین شرایطی، متوقف کردن سرویس برای پشتیبان‌گیری سنتی نه تنها غیرعملی است، بلکه می‌تواند به از دست رفتن درآمد یا نارضایتی کاربران منجر شود. دستور VACUUM INTO در SQLite، که از نسخه 3.27.0 (سال 2019) به بعد پشتیبانی می‌شود، یک راه‌حل رسمی و قدرتمند برای ایجاد پشتیبان آنلاین (Online Backup) ارائه می‌دهد. این دستور اجازه می‌دهد تا یک کپی کامل و سازگار از پایگاه داده را بدون مسدود کردن عملیات خواندن ایجاد کنید.
در این مقاله، به بررسی نحوه استفاده از VACUUM INTO در محیط .NET با تمرکز بر Providerهای ADO.NET می‌پردازیم. هدف ما ارائه یک راهنمای عملی برای توسعه‌دهندگان است که بتوانند این ویژگی را در محیط‌های تولید (Production) پیاده‌سازی کنند. با توجه به اهمیت امنیت و کارایی، نکات کلیدی مانند تنظیمات پیش‌نیاز، محدودیت‌ها و بهترین شیوه‌ها را پوشش می‌دهیم. اگر شما توسعه‌دهنده‌ای هستید که با SQLite کار می‌کنید، این مقاله به شما کمک می‌کند تا پشتیبان‌گیری را به شیوه‌ای ایمن و کارآمد انجام دهید.

پشتیبانی از VACUUM INTO در SQLite و Providerهای .NET
دستور VACUUM INTO به صورت زیر استفاده می‌شود:
VACUUM INTO 'path/to/backup.db';
این دستور پایگاه داده فعلی را به طور کامل بازنویسی (Rewrite) کرده و یک فایل پشتیبان جدید ایجاد می‌کند. برای استفاده از آن، نسخه موتور SQLite باید حداقل 3.27.0 باشد. این ویژگی کاملاً رسمی است و در اسناد SQLite تأیید شده.
در سمت .NET، Providerهای ADO.NET نقش کلیدی دارند:
  • Microsoft.Data.Sqlite (Provider رسمی مایکروسافت): این Provider از اجرای دستورات SQL خام به طور کامل پشتیبانی می‌کند. اگر نسخه داخلی SQLite آن جدید باشد، VACUUM INTO بدون مشکل اجرا می‌شود. نکته مهم: مسیر فایل پشتیبان باید قابل نوشتن باشد و پایگاه داده نباید در حالت تراکنش (Transaction) فعال باشد.
  • System.Data.SQLite (پروژه قدیمی‌تر): اگر نسخه بیلد آن بر پایه SQLite جدید باشد، دستور پشتیبانی می‌شود. با این حال، برخی بسته‌های قدیمی NuGet هنوز از نسخه‌های قدیمی SQLite استفاده می‌کنند، که ممکن است منجر به عدم پشتیبانی شود. بنابراین، همیشه نسخه را بررسی کنید.

نکته: انتخاب Provider مناسب می‌تواند بر کارایی کلی تأثیر بگذارد. Microsoft.Data.Sqlite به دلیل ادغام بهتر با اکوسیستم مایکروسافت، اغلب گزینه ارجح برای پروژه‌های مدرن است.

محدودیت‌ها و هشدارها
VACUUM INTO قدرتمند است، اما محدودیت‌هایی دارد که باید به آنها توجه کرد:
  • عدم اجرا در تراکنش: دستور نمی‌تواند داخل یک تراکنش فعال اجرا شود، زیرا نیاز به دسترسی انحصاری (Exclusive) دارد.
  • اتصال انحصاری: استخر اتصال (Connection Pooling) می‌تواند مشکل‌ساز شود، زیرا ممکن است اتصالات اشتراکی ایجاد کند. پیشنهاد می‌شود Pooling را خاموش کنید:
Data Source=main.db;Pooling=False;
  • مسیر فایل ثابت: مسیر پشتیبان باید به صورت مستقیم (Literal) در دستور SQL مشخص شود؛ نمی‌توان از پارامترهای SQL (مانند @path) استفاده کرد، زیرا SQLite اجازه Bind کردن مسیر فایل را نمی‌دهد. بنابراین، کد زیر کار نمی‌کند:
VACUUM INTO @path
این محدودیت‌ها برای جلوگیری از مشکلات امنیتی مانند تزریق مسیر (Path Injection) طراحی شده‌اند. اگر توضیح بیشتری نیاز باشد، می‌توان گفت که این ویژگی SQLite را در برابر حملات احتمالی مقاوم می‌کند، اما توسعه‌دهندگان را ملزم به مدیریت دقیق مسیرها می‌نماید.

تنظیمات حیاتی برای پشتیبان‌گیری آنلاین
برای موفقیت در پشتیبان‌گیری آنلاین، تنظیمات زیر ضروری هستند:
  • حالت WAL (Write-Ahead Logging): این حالت اجباری است، زیرا اجازه عملیات خواندن همزمان با نوشتن را می‌دهد و قفل‌های طولانی (Lock) را کاهش می‌دهد. بدون WAL، نوشتن می‌تواند تأخیر (Latency) بالایی ایجاد کند:
PRAGMA journal_mode = WAL;
  • زمان انتظار شلوغی (Busy Timeout): برای جلوگیری از شکست عملیات در شرایط بار بالا (Load)، زمان انتظار را تنظیم کنید:
connection.Execute("PRAGMA busy_timeout = 10000;");
  • خاموش کردن Pooling: همان‌طور که ذکر شد، برای اتصال انحصاری.

علاوه بر این، می‌توان به اهمیت WAL در محیط‌های چندنخی (Multi-Threaded) اشاره کرد: WAL نه تنها پشتیبان‌گیری را تسهیل می‌کند، بلکه عملکرد کلی پایگاه داده را در سناریوهای نوشتاری سنگین بهبود می‌بخشد.

نکات امنیتی و عملیاتی در سرور
در محیط تولید، امنیت و کارایی اولویت دارند:
  • مسیر پشتیبان: ترجیحاً روی دیسک فیزیکی جداگانه باشد تا فشار ورودی/خروجی (IO) توزیع شود. مجوز نوشتن را بررسی کنید و نام فایل را با زمان‌بندی (Timestamp) انتخاب کنید، مانند backup_20260129_021300.db.
  • فشار IO: دستور VACUUM INTO پایگاه داده را کامل بازنویسی می‌کند، بنابراین برای پایگاه‌های بزرگ (بیش از 10 گیگابایت)، بهتر است در ساعات کم‌بار (Off-Peak) اجرا شود یا از کارهای زمان‌بندی‌شده (Cron Jobs) استفاده کنید.
اگر پایگاه داده بسیار بزرگ یا نوشتاری سنگین باشد، VACUUM INTO ممکن است مناسب نباشد. در چنین مواردی، جایگزین‌هایی مانند API پشتیبان‌گیری SQLite (sqlite3_backup API) یا پشتیبان‌گیری افزایشی (Incremental Backup) پیشنهاد می‌شود.

تست سلامت پشتیبان
پس از ایجاد پشتیبان، بررسی سلامت آن ضروری است. می‌توانید از دستور زیر استفاده کنید:
PRAGMA quick_check;
این دستور سریع‌تر از integrity_check است و در محیط تولید مناسب‌تر. اگر نتیجه "ok" نباشد، پشتیبان نامعتبر است.

پیاده‌سازی عملی: یک متد تولیدگرا
برای پیاده‌سازی، یک متد asynchronous پیشنهاد می‌شود که تمام بهترین شیوه‌ها را پوشش دهد. این متد بدون توقف سرویس، مقاوم به رقابت (Contention) و با بررسی سلامت خودکار است:
using Microsoft.Data.Sqlite;
using System.Globalization;

public static class SqliteBackupHelper
{
    public static async Task CreateOnlineSqliteBackupAsync(
        string sourceConnectionString,
        string backupFilePath,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(sourceConnectionString);
        ArgumentException.ThrowIfNullOrWhiteSpace(backupFilePath);
        //  VACUUM INTO پارامتر نمی‌پذیرد
        backupFilePath = backupFilePath.Replace("'", "''");
        var connectionString =
            sourceConnectionString.Contains("Pooling=", StringComparison.OrdinalIgnoreCase)
                ? sourceConnectionString
                : $"{sourceConnectionString};Pooling=False";
        await using var connection = new SqliteConnection(connectionString);
        await connection.OpenAsync(cancellationToken);
        await using var command = connection.CreateCommand();
        command.CommandTimeout = 0; // vacuum ممکنه طولانی باشه
        command.CommandText = $"""
            -- 1. اطمینان از WAL
            PRAGMA journal_mode = WAL;
            -- 2. جلوگیری از fail زیر load
            PRAGMA busy_timeout = 10000;
            -- 3. بکاپ آنلاین
            VACUUM INTO '{backupFilePath}';
        """;
        await command.ExecuteNonQueryAsync(cancellationToken);
        // 🔍 Health Check بکاپ
        await ValidateBackupAsync(backupFilePath, cancellationToken);
    }

    private static async Task ValidateBackupAsync(
        string backupFilePath,
        CancellationToken cancellationToken)
    {
        var backupConnectionString =
            $"Data Source={backupFilePath};Mode=ReadOnly;Pooling=False";
        await using var backupConnection = new SqliteConnection(backupConnectionString);
        await backupConnection.OpenAsync(cancellationToken);
        await using var command = backupConnection.CreateCommand();
        command.CommandText = "PRAGMA quick_check;";
        command.CommandTimeout = 30;
        var result = await command.ExecuteScalarAsync(cancellationToken);
        if (!string.Equals(result?.ToString(), "ok", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException(
                $"SQLite backup integrity check failed: {result}");
        }
    }
}
این کد ساده و خوانا است، با تمرکز بر atomic بودن عملیات و مدیریت خطاها. برای استفاده، می‌توانید آن را در یک برنامه کنسولی یا Job فراخوانی کنید، مانند مثال زیر:
var sourceConnectionString = "Data Source=/var/app/data/main.db;Cache=Shared;";
var backupPath = Path.Combine("/var/app/backups", $"main_{DateTime.UtcNow:yyyyMMdd_HHmmss}.db");
await SqliteBackupHelper.CreateOnlineSqliteBackupAsync(sourceConnectionString, backupPath);
نکته: استفاده از CancellationToken اجازه می‌دهد تا در صورت خاموش شدن سرویس، عملیات به طور ایمن متوقف شود بدون ایجاد فایل ناقص.

نتیجه‌گیری
در نهایت، VACUUM INTO همراه با حالت WAL، یک گزینه عالی برای پشتیبان‌گیری آنلاین از SQLite در محیط .NET است، به ویژه در سرورهای در حال سرویس‌دهی. این روش atomic، non-blocking برای خواندن و سازگار با WAL است، که آن را برای کاربردهای تولید مناسب می‌کند. با رعایت تنظیماتی مانند خاموش کردن Pooling، تنظیم Busy Timeout و بررسی سلامت، می‌توانید از ایمنی داده‌ها اطمینان حاصل کنید. اگر پایگاه داده شما بزرگ یا نوشتاری سنگین است، جایگزین‌هایی مانند API پشتیبان‌گیری را در نظر بگیرید. این رویکرد نه تنها کارایی را حفظ می‌کند، بلکه توسعه‌دهندگان را به سمت شیوه‌های پایدار و امن هدایت می‌نماید.