ذخیره‌سازی محلی با SQLite در .NET MAUI: راهنمای عملی با Repository و MVVM

آموزش گام‌به‌گام استفاده از SQLite در .NET MAUI با الگوی Repository، عملیات CRUD غیرهمزمان، تزریق وابستگی و تکنیک‌های پیشرفته مثل WAL و Migration. همراه با پروژه عملی مدیریت وظایف.

اگر تا حالا با توسعه اپلیکیشن‌های موبایل کار کرده باشید، احتمالاً خودتون هم متوجه شدید که تقریباً هر اپلیکیشنی — از یک اپ ساده یادداشت‌برداری گرفته تا یک فروشگاه آنلاین — به ذخیره‌سازی داده محلی نیاز داره. تنظیمات کاربر، لیست وظایف، سبد خرید، داده‌های آفلاین... همه اینا باید یه جایی ذخیره بشن.

و خب، SQLite محبوب‌ترین و اثبات‌شده‌ترین گزینه برای این کاره.

در مقاله قبلی درباره معماری MVVM در .NET MAUI صحبت کردیم. حالا وقتشه یک قدم جلوتر بریم و لایه داده (Data Layer) اپلیکیشن رو با SQLite پیاده‌سازی کنیم — البته با رعایت اصول معماری تمیز و الگوی Repository.

توی این مقاله از صفر تا صد با SQLite در .NET MAUI آشنا می‌شید: نصب و پیکربندی، تعریف مدل‌های داده، ساخت سرویس دیتابیس Async، پیاده‌سازی الگوی Repository، ادغام با تزریق وابستگی و MVVM، و نکات پیشرفته‌ای مثل WAL و Migration.

چرا SQLite برای اپلیکیشن‌های .NET MAUI؟

قبل از اینکه بریم سراغ کدنویسی، بذارید یه نگاه سریع بندازیم به دلایل انتخاب SQLite:

  • بدون نیاز به سرور: SQLite یک دیتابیس فایل‌محور (embedded) است. کل دیتابیس در یک فایل روی دستگاه ذخیره می‌شه و نیازی به سرور جداگانه نداره.
  • چندپلتفرمی: روی Android، iOS، macOS و Windows بدون هیچ تغییری کار می‌کنه — دقیقاً مثل خود .NET MAUI.
  • عملکرد بالا: برای عملیات خواندن و نوشتن محلی فوق‌العاده سریعه. حجمش هم کمتر از ۵۰۰ کیلوبایته.
  • سازگاری با ACID: تمام تراکنش‌ها از اصول ACID پیروی می‌کنن و داده‌هاتون همیشه سالم می‌مونه.
  • اکوسیستم غنی: کتابخانه sqlite-net-pcl یک ORM سبک‌وزن عالی برای کار با SQLite در .NET فراهم می‌کنه.

راستش من شخصاً توی چندین پروژه موبایل از SQLite استفاده کردم و هر بار از سادگی و قابلیت اطمینانش شگفت‌زده شدم.

نصب و راه‌اندازی SQLite در پروژه .NET MAUI

برای شروع، باید دو پکیج NuGet رو نصب کنید:

dotnet add package sqlite-net-pcl --version 1.9.172
dotnet add package SQLitePCLRaw.bundle_green --version 2.1.10

پکیج sqlite-net-pcl همون ORM اصلیه که باهاش کار می‌کنیم. پکیج SQLitePCLRaw.bundle_green هم provider بومی SQLite رو فراهم می‌کنه تا روی تمام پلتفرم‌ها درست کار کنه.

اگر از CommunityToolkit.Mvvm در مقاله قبلی استفاده کردید، ساختار پروژه‌تون آماده‌ست. فقط کافیه پوشه‌های زیر رو اضافه کنید:

TaskManagerApp/
├── Models/
├── ViewModels/
├── Views/
├── Services/
│   ├── Database/
│   └── Repositories/
└── MauiProgram.cs

تعریف مدل‌های داده با Attributeهای SQLite

خب، اولین قدم تعریف مدل‌های داده‌ست. SQLite-net از Attributeها برای نگاشت کلاس‌های C# به جداول دیتابیس استفاده می‌کنه:

using SQLite;

namespace TaskManagerApp.Models;

public class TaskItem
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }

    [MaxLength(200)]
    [NotNull]
    public string Title { get; set; } = string.Empty;

    [MaxLength(1000)]
    public string Description { get; set; } = string.Empty;

    public DateTime DueDate { get; set; }

    public bool IsCompleted { get; set; }

    [Indexed]
    public int CategoryId { get; set; }

    public DateTime CreatedAt { get; set; }

    public DateTime UpdatedAt { get; set; }
}

public class Category
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }

    [MaxLength(100)]
    [NotNull]
    [Unique]
    public string Name { get; set; } = string.Empty;

    [MaxLength(7)]
    public string ColorHex { get; set; } = "#3498db";
}

یه نکته‌ای که شاید بدیهی به نظر برسه ولی مهمه: نام‌گذاری Attributeها خیلی گویاست. بذارید سریع مرورشون کنیم:

  • [PrimaryKey, AutoIncrement]: کلید اصلی با افزایش خودکار — هر رکورد یک شناسه یکتا می‌گیره.
  • [MaxLength]: حداکثر طول رشته. برای بهینه‌سازی فضای ذخیره‌سازی مفیده.
  • [NotNull]: فیلد نمی‌تونه خالی باشه.
  • [Indexed]: ایندکس برای جستجوی سریع‌تر — روی فیلدهایی بذارید که زیاد فیلتر می‌شن.
  • [Unique]: مقدار این فیلد باید در کل جدول یکتا باشه.

ساخت سرویس دیتابیس Async

حالا می‌رسیم به مهم‌ترین بخش. نکته کلیدی اینه که همیشه از SQLiteAsyncConnection استفاده کنید تا رابط کاربری قفل نشه. این یکی از اون اشتباهاتیه که خیلی‌ها اول کار مرتکب می‌شن و بعد متوجه می‌شن چرا اپلیکیشنشون هنگ می‌کنه.

using SQLite;

namespace TaskManagerApp.Services.Database;

public interface IDatabaseService
{
    SQLiteAsyncConnection Database { get; }
    Task InitializeAsync();
}

public class DatabaseService : IDatabaseService
{
    private SQLiteAsyncConnection _database;
    private bool _initialized;

    public SQLiteAsyncConnection Database =>
        _database ?? throw new InvalidOperationException(
            "Database not initialized. Call InitializeAsync first.");

    public async Task InitializeAsync()
    {
        if (_initialized) return;

        var dbPath = Path.Combine(
            FileSystem.AppDataDirectory, "taskmanager.db3");

        _database = new SQLiteAsyncConnection(dbPath,
            SQLiteOpenFlags.ReadWrite |
            SQLiteOpenFlags.Create |
            SQLiteOpenFlags.SharedCache);

        // فعال‌سازی WAL برای عملکرد بهتر
        await _database.EnableWriteAheadLoggingAsync();

        // ساخت جداول
        await _database.CreateTableAsync<TaskItem>();
        await _database.CreateTableAsync<Category>();

        _initialized = true;
    }
}

چند نکته مهم توی این کد هست:

  • FileSystem.AppDataDirectory: مسیر مخصوص اپلیکیشن برای ذخیره فایل‌ها — روی هر پلتفرم به مسیر مناسب اشاره می‌کنه.
  • SQLiteOpenFlags: فلگ SharedCache امکان استفاده هم‌زمان چند اتصال از یک کش رو فراهم می‌کنه.
  • EnableWriteAheadLoggingAsync: حالت WAL عملکرد نوشتن رو به شکل چشمگیری بهبود می‌ده. به جای نوشتن مستقیم روی فایل اصلی، تغییرات اول در یک فایل WAL جداگانه نوشته می‌شن و بعد ادغام می‌شن.
  • CreateTableAsync: اگر جدول وجود داشته باشه، اسکیمای آن بررسی و در صورت نیاز به‌روزرسانی می‌شه.

پیاده‌سازی الگوی Repository

الگوی Repository لایه‌ای بین منطق کسب‌وکار و دسترسی به داده ایجاد می‌کنه. شاید در نگاه اول اضافه‌کاری به نظر برسه، ولی وقتی پروژه بزرگ‌تر می‌شه واقعاً ارزشش رو داره — کد تمیزتر، قابل تست‌تر و قابل نگهداری‌تر می‌شه.

اول یک اینترفیس جنریک تعریف می‌کنیم:

namespace TaskManagerApp.Services.Repositories;

public interface IRepository<T> where T : new()
{
    Task<List<T>> GetAllAsync();
    Task<T> GetByIdAsync(int id);
    Task<int> AddAsync(T entity);
    Task<int> UpdateAsync(T entity);
    Task<int> DeleteAsync(T entity);
    Task<int> CountAsync();
}

بعد پیاده‌سازی جنریکش:

using SQLite;

namespace TaskManagerApp.Services.Repositories;

public class Repository<T> : IRepository<T> where T : new()
{
    private readonly SQLiteAsyncConnection _db;

    public Repository(IDatabaseService databaseService)
    {
        _db = databaseService.Database;
    }

    public async Task<List<T>> GetAllAsync()
    {
        return await _db.Table<T>().ToListAsync();
    }

    public async Task<T> GetByIdAsync(int id)
    {
        return await _db.FindAsync<T>(id);
    }

    public async Task<int> AddAsync(T entity)
    {
        return await _db.InsertAsync(entity);
    }

    public async Task<int> UpdateAsync(T entity)
    {
        return await _db.UpdateAsync(entity);
    }

    public async Task<int> DeleteAsync(T entity)
    {
        return await _db.DeleteAsync(entity);
    }

    public async Task<int> CountAsync()
    {
        return await _db.Table<T>().CountAsync();
    }
}

تا اینجا خوبه، ولی یه Repository جنریک به تنهایی کافی نیست. برای مدل TaskItem یک Repository تخصصی می‌سازیم که قابلیت‌های بیشتری داره:

namespace TaskManagerApp.Services.Repositories;

public interface ITaskRepository : IRepository<TaskItem>
{
    Task<List<TaskItem>> GetByCategoryAsync(int categoryId);
    Task<List<TaskItem>> GetPendingAsync();
    Task<List<TaskItem>> GetCompletedAsync();
    Task<List<TaskItem>> SearchAsync(string query);
    Task ToggleCompletionAsync(int taskId);
}

public class TaskRepository : Repository<TaskItem>, ITaskRepository
{
    private readonly SQLiteAsyncConnection _db;

    public TaskRepository(IDatabaseService databaseService)
        : base(databaseService)
    {
        _db = databaseService.Database;
    }

    public async Task<List<TaskItem>> GetByCategoryAsync(int categoryId)
    {
        return await _db.Table<TaskItem>()
            .Where(t => t.CategoryId == categoryId)
            .OrderByDescending(t => t.CreatedAt)
            .ToListAsync();
    }

    public async Task<List<TaskItem>> GetPendingAsync()
    {
        return await _db.Table<TaskItem>()
            .Where(t => !t.IsCompleted)
            .OrderBy(t => t.DueDate)
            .ToListAsync();
    }

    public async Task<List<TaskItem>> GetCompletedAsync()
    {
        return await _db.Table<TaskItem>()
            .Where(t => t.IsCompleted)
            .OrderByDescending(t => t.UpdatedAt)
            .ToListAsync();
    }

    public async Task<List<TaskItem>> SearchAsync(string query)
    {
        var lowerQuery = query.ToLower();
        return await _db.Table<TaskItem>()
            .Where(t => t.Title.ToLower().Contains(lowerQuery))
            .ToListAsync();
    }

    public async Task ToggleCompletionAsync(int taskId)
    {
        var task = await GetByIdAsync(taskId);
        if (task != null)
        {
            task.IsCompleted = !task.IsCompleted;
            task.UpdatedAt = DateTime.UtcNow;
            await UpdateAsync(task);
        }
    }
}

به متد ToggleCompletionAsync دقت کنید. این نوع متدهای تخصصی دقیقاً دلیل ساخت Repository اختصاصی هستن — عملیاتی که مختص یک مدل خاصه و توی جنریک جا نمی‌شه.

ادغام با تزریق وابستگی (Dependency Injection)

خب، حالا باید تمام سرویس‌ها و Repository‌ها رو در MauiProgram.cs ثبت کنیم. اگر مقاله قبلی درباره MVVM و تزریق وابستگی رو خوندید، این الگو براتون آشناست:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // ثبت سرویس دیتابیس به صورت Singleton
        builder.Services.AddSingleton<IDatabaseService, DatabaseService>();

        // ثبت Repositoryها
        builder.Services.AddSingleton<ITaskRepository, TaskRepository>();
        builder.Services.AddSingleton<IRepository<Category>, Repository<Category>>();

        // ثبت ViewModelها
        builder.Services.AddTransient<TaskListViewModel>();
        builder.Services.AddTransient<TaskDetailViewModel>();

        // ثبت Pageها
        builder.Services.AddTransient<TaskListPage>();
        builder.Services.AddTransient<TaskDetailPage>();

        return builder.Build();
    }
}

یه نکته مهم اینجا هست که خیلی‌ها اولش بهش دقت نمی‌کنن: DatabaseService و Repository‌ها رو Singleton ثبت کردیم چون فقط باید یک اتصال دیتابیس توی کل اپلیکیشن وجود داشته باشه. ولی ViewModel‌ها و Page‌ها Transient هستن تا هر بار نمونه تازه‌ای ساخته بشه.

مقداردهی اولیه دیتابیس در App.xaml.cs

دیتابیس باید قبل از استفاده مقداردهی بشه. بهترین جا برای این کار App.xaml.cs هست:

public partial class App : Application
{
    public App(IDatabaseService databaseService)
    {
        InitializeComponent();

        // مقداردهی اولیه دیتابیس
        Task.Run(async () => await databaseService.InitializeAsync())
            .GetAwaiter().GetResult();
    }
}

استفاده از Repository در ViewModel با CommunityToolkit.Mvvm

حالا می‌رسیم به بهترین بخش: ترکیب Repository با ViewModel و CommunityToolkit.Mvvm. اگر مقاله قبلی رو خوندید، با [ObservableProperty] و [RelayCommand] آشنایید:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;

namespace TaskManagerApp.ViewModels;

public partial class TaskListViewModel : ObservableObject
{
    private readonly ITaskRepository _taskRepository;

    public TaskListViewModel(ITaskRepository taskRepository)
    {
        _taskRepository = taskRepository;
    }

    [ObservableProperty]
    public partial ObservableCollection<TaskItem> Tasks { get; set; } = [];

    [ObservableProperty]
    public partial bool IsLoading { get; set; }

    [ObservableProperty]
    public partial string SearchQuery { get; set; } = string.Empty;

    [RelayCommand]
    private async Task LoadTasksAsync()
    {
        IsLoading = true;
        var tasks = await _taskRepository.GetPendingAsync();
        Tasks = new ObservableCollection<TaskItem>(tasks);
        IsLoading = false;
    }

    [RelayCommand]
    private async Task SearchTasksAsync()
    {
        if (string.IsNullOrWhiteSpace(SearchQuery))
        {
            await LoadTasksAsync();
            return;
        }

        IsLoading = true;
        var results = await _taskRepository.SearchAsync(SearchQuery);
        Tasks = new ObservableCollection<TaskItem>(results);
        IsLoading = false;
    }

    [RelayCommand]
    private async Task ToggleTaskAsync(TaskItem task)
    {
        await _taskRepository.ToggleCompletionAsync(task.Id);
        await LoadTasksAsync();
    }

    [RelayCommand]
    private async Task DeleteTaskAsync(TaskItem task)
    {
        bool confirm = await Shell.Current.DisplayAlert(
            "حذف وظیفه",
            $"آیا از حذف \"{task.Title}\" مطمئن هستید؟",
            "بله", "خیر");

        if (confirm)
        {
            await _taskRepository.DeleteAsync(task);
            Tasks.Remove(task);
        }
    }
}

و حالا ViewModel مربوط به صفحه جزئیات وظیفه. این ViewModel یه ذره پیچیده‌تره چون هم حالت ایجاد و هم حالت ویرایش رو مدیریت می‌کنه:

public partial class TaskDetailViewModel : ObservableObject, IQueryAttributable
{
    private readonly ITaskRepository _taskRepository;
    private readonly IRepository<Category> _categoryRepository;

    public TaskDetailViewModel(
        ITaskRepository taskRepository,
        IRepository<Category> categoryRepository)
    {
        _taskRepository = taskRepository;
        _categoryRepository = categoryRepository;
    }

    [ObservableProperty]
    public partial string Title { get; set; } = string.Empty;

    [ObservableProperty]
    public partial string Description { get; set; } = string.Empty;

    [ObservableProperty]
    public partial DateTime DueDate { get; set; } = DateTime.Today;

    [ObservableProperty]
    public partial ObservableCollection<Category> Categories { get; set; } = [];

    [ObservableProperty]
    public partial Category SelectedCategory { get; set; }

    private int _editingTaskId;

    public void ApplyQueryAttributes(IDictionary<string, object> query)
    {
        if (query.TryGetValue("taskId", out var idObj)
            && int.TryParse(idObj?.ToString(), out var id))
        {
            _editingTaskId = id;
            _ = LoadTaskAsync(id);
        }
    }

    private async Task LoadTaskAsync(int id)
    {
        var task = await _taskRepository.GetByIdAsync(id);
        if (task != null)
        {
            Title = task.Title;
            Description = task.Description;
            DueDate = task.DueDate;
        }
    }

    [RelayCommand]
    private async Task LoadCategoriesAsync()
    {
        var cats = await _categoryRepository.GetAllAsync();
        Categories = new ObservableCollection<Category>(cats);
    }

    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync()
    {
        var task = new TaskItem
        {
            Id = _editingTaskId,
            Title = Title,
            Description = Description,
            DueDate = DueDate,
            CategoryId = SelectedCategory?.Id ?? 0,
            UpdatedAt = DateTime.UtcNow
        };

        if (_editingTaskId > 0)
            await _taskRepository.UpdateAsync(task);
        else
        {
            task.CreatedAt = DateTime.UtcNow;
            await _taskRepository.AddAsync(task);
        }

        await Shell.Current.GoToAsync("..");
    }

    private bool CanSave() => !string.IsNullOrWhiteSpace(Title);
}

اتصال View به ViewModel با Data Binding

صفحه XAML لیست وظایف رو ببینید. نکته جالبش استفاده از SwipeView برای حذف با کشیدن انگشته (که واقعاً تجربه کاربری خوبی می‌سازه):

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:TaskManagerApp.ViewModels"
             x:Class="TaskManagerApp.Views.TaskListPage"
             x:DataType="vm:TaskListViewModel"
             Title="وظایف من">

    <Grid RowDefinitions="Auto,*" Padding="16">
        <!-- نوار جستجو -->
        <SearchBar Grid.Row="0"
                   Text="{Binding SearchQuery}"
                   SearchCommand="{Binding SearchTasksCommand}"
                   Placeholder="جستجوی وظایف..." />

        <!-- لیست وظایف -->
        <CollectionView Grid.Row="1"
                        ItemsSource="{Binding Tasks}"
                        SelectionMode="None">
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="models:TaskItem">
                    <SwipeView>
                        <SwipeView.RightItems>
                            <SwipeItems>
                                <SwipeItem Text="حذف"
                                           BackgroundColor="Red"
                                           Command="{Binding Source={RelativeSource
                                               AncestorType={x:Type vm:TaskListViewModel}},
                                               Path=DeleteTaskCommand}"
                                           CommandParameter="{Binding .}" />
                            </SwipeItems>
                        </SwipeView.RightItems>
                        <Grid Padding="8" ColumnDefinitions="Auto,*,Auto">
                            <CheckBox Grid.Column="0"
                                      IsChecked="{Binding IsCompleted}">
                                <CheckBox.Behaviors>
                                    <!-- تعامل با ViewModel -->
                                </CheckBox.Behaviors>
                            </CheckBox>
                            <VerticalStackLayout Grid.Column="1">
                                <Label Text="{Binding Title}"
                                       FontSize="16"
                                       FontAttributes="Bold" />
                                <Label Text="{Binding DueDate, StringFormat='{0:yyyy/MM/dd}'}"
                                       FontSize="12"
                                       TextColor="Gray" />
                            </VerticalStackLayout>
                        </Grid>
                    </SwipeView>
                </DataTemplate>
            </CollectionView.ItemTemplate>

            <CollectionView.EmptyView>
                <Label Text="هنوز وظیفه‌ای ثبت نشده!"
                       HorizontalOptions="Center"
                       VerticalOptions="Center" />
            </CollectionView.EmptyView>
        </CollectionView>
    </Grid>
</ContentPage>

یه نکته مهم: از x:DataType برای Compiled Bindings استفاده شده. این کار عملکرد Data Binding رو به شکل محسوسی بهبود می‌ده چون عبارات Binding در زمان کامپایل بررسی و بهینه‌سازی می‌شن. اگه بدون این Attribute کار کنید، Binding در زمان اجرا با Reflection انجام می‌شه که خیلی کندتره.

عملیات CRUD کامل: ساخت، خواندن، بروزرسانی و حذف

بیایید یه مرور سریع داشته باشیم بر تمام عملیات CRUD:

// ایجاد (Create)
var newTask = new TaskItem
{
    Title = "خرید هفتگی",
    Description = "میوه، سبزیجات و لبنیات",
    DueDate = DateTime.Today.AddDays(1),
    CreatedAt = DateTime.UtcNow,
    UpdatedAt = DateTime.UtcNow
};
await taskRepository.AddAsync(newTask);

// خواندن (Read)
var allTasks = await taskRepository.GetAllAsync();
var singleTask = await taskRepository.GetByIdAsync(1);
var pendingTasks = await taskRepository.GetPendingAsync();

// بروزرسانی (Update)
singleTask.Title = "خرید هفتگی — به‌روز شده";
singleTask.UpdatedAt = DateTime.UtcNow;
await taskRepository.UpdateAsync(singleTask);

// حذف (Delete)
await taskRepository.DeleteAsync(singleTask);

ساده و تمیز. هر عملیات یک خط کده و Repository پیچیدگی‌های دیتابیس رو از بقیه کد مخفی می‌کنه.

تکنیک‌های پیشرفته SQLite در .NET MAUI

۱. استفاده از تراکنش‌ها (Transactions)

وقتی چندین عملیات باید به صورت اتمی اجرا بشن (یعنی یا همه انجام بشن یا هیچ‌کدوم)، از تراکنش استفاده کنید:

public async Task ImportTasksAsync(List<TaskItem> tasks)
{
    await _db.RunInTransactionAsync(conn =>
    {
        foreach (var task in tasks)
        {
            conn.Insert(task);
        }
    });
}

اگر هر کدوم از عملیات داخل تراکنش شکست بخوره، تمام تغییرات برگردانده (Rollback) می‌شن. این مخصوصاً وقتی دارید داده‌ها رو import می‌کنید خیلی مهمه.

۲. کوئری‌های خام SQL

گاهی LINQ کافی نیست. مثلاً وقتی می‌خواید کوئری‌های پیچیده‌تر بزنید یا از قابلیت‌های خاص SQLite استفاده کنید:

public async Task<List<TaskItem>> GetOverdueTasksAsync()
{
    var now = DateTime.UtcNow;
    return await _db.QueryAsync<TaskItem>(
        "SELECT * FROM TaskItem WHERE DueDate < ? AND IsCompleted = 0 ORDER BY DueDate",
        now);
}

۳. مدیریت نسخه‌های دیتابیس (Migration)

یکی از چالش‌های رایج — و راستش یکی از اون چیزایی که اگه از اول بهش فکر نکنید بعداً دردسرساز می‌شه — تغییر اسکیمای دیتابیس توی نسخه‌های جدید اپلیکیشنه. SQLite-net متد CreateTableAsync ستون‌های جدید رو اضافه می‌کنه، ولی برای تغییرات پیچیده‌تر نیاز به Migration دستی دارید:

public async Task MigrateAsync()
{
    var currentVersion = Preferences.Get("db_version", 0);

    if (currentVersion < 1)
    {
        await _database.ExecuteAsync(
            "ALTER TABLE TaskItem ADD COLUMN Priority INTEGER DEFAULT 0");
        Preferences.Set("db_version", 1);
    }

    if (currentVersion < 2)
    {
        await _database.ExecuteAsync(
            "ALTER TABLE TaskItem ADD COLUMN ReminderAt TEXT");
        Preferences.Set("db_version", 2);
    }
}

نکته‌ای که من همیشه رعایت می‌کنم: هر Migration رو شماره‌گذاری کنید و هرگز Migration‌های قبلی رو تغییر ندید. فقط Migration جدید اضافه کنید.

۴. ایندکس‌گذاری برای عملکرد بهتر

برای جداول با حجم داده بالا، ایندکس‌گذاری صحیح تفاوت چشمگیری ایجاد می‌کنه. مخصوصاً وقتی روی یک فیلد مرتب فیلتر می‌زنید:

// ایندکس ترکیبی روی مدل
public class TaskItem
{
    // ... سایر فیلدها

    [Indexed(Name = "IX_Task_Category_Completed", Order = 1)]
    public int CategoryId { get; set; }

    [Indexed(Name = "IX_Task_Category_Completed", Order = 2)]
    public bool IsCompleted { get; set; }
}

نکات عملکردی مهم

این بخش رو حتماً بخونید. رعایت این نکات تفاوت بین یه اپلیکیشن روان و یه اپلیکیشن کُنده:

  • همیشه از عملیات Async استفاده کنید: هرگز از SQLiteConnection سنکرون روی ترد اصلی استفاده نکنید. باعث فریز شدن رابط کاربری می‌شه و کاربر فکر می‌کنه اپلیکیشن هنگ کرده.
  • WAL رو فعال کنید: Write-Ahead Logging عملکرد نوشتن رو تا چندین برابر بهبود می‌ده و امکان خواندن همزمان حین نوشتن رو فراهم می‌کنه.
  • از تراکنش برای عملیات دسته‌ای استفاده کنید: به جای درج تک‌تک رکوردها، از RunInTransactionAsync استفاده کنید. تفاوت سرعت واقعاً قابل توجهه.
  • ایندکس‌ها رو هوشمندانه اضافه کنید: فقط روی ستون‌هایی که در شرط WHERE یا ORDER BY استفاده می‌شن ایندکس بذارید. ایندکس اضافه هم هزینه نوشتن رو بالا می‌بره.
  • اتصال دیتابیس رو Singleton نگه دارید: ساخت اتصال جدید هزینه‌بره. یک نمونه واحد از SQLiteAsyncConnection در کل اپلیکیشن کافیه.

سوالات متداول

آیا SQLite برای اپلیکیشن‌های .NET MAUI با حجم داده بالا مناسب است؟

بله، کاملاً. SQLite به راحتی از دیتابیس‌های تا چندین گیگابایت پشتیبانی می‌کنه. البته برای حجم داده بالا باید ایندکس‌گذاری صحیح انجام بدید، از WAL استفاده کنید و کوئری‌ها رو بهینه‌سازی کنید. برای اپلیکیشن‌هایی با صدها هزار رکورد، SQLite بدون مشکل کار می‌کنه.

تفاوت sqlite-net-pcl و Entity Framework Core برای .NET MAUI چیست؟

پکیج sqlite-net-pcl یک ORM سبک‌وزنه که مستقیماً با SQLite کار می‌کنه و برای اکثر سناریوهای موبایل کافیه. EF Core قدرتمندتره و از Migration خودکار، روابط پیچیده و LINQ کامل پشتیبانی می‌کنه، ولی حجم بیشتری به اپلیکیشن اضافه می‌کنه. توصیه من برای اپلیکیشن‌های موبایل ساده تا متوسط: sqlite-net-pcl.

چگونه می‌توان داده‌های SQLite را با یک سرور آنلاین همگام‌سازی (Sync) کرد؟

برای همگام‌سازی دوطرفه، می‌تونید از فریمورک Microsoft.Datasync یا کتابخانه‌های متن‌باز مثل NubeSync استفاده کنید. یه رویکرد دیگه هم هست: پیاده‌سازی دستی با استفاده از timestamp آخرین همگام‌سازی و ارسال تغییرات از طریق REST API. در هر صورت، مدیریت تعارض (Conflict Resolution) مهم‌ترین چالشه و باید از اول استراتژی مشخصی براش داشته باشید.

آیا SQLite از روابط بین جداول (Foreign Key) پشتیبانی می‌کند؟

بله. ولی در sqlite-net-pcl باید روابط رو خودتون مدیریت کنید. فیلد CategoryId در مدل TaskItem مثال خوبی از رابطه یک‌به‌چنده. اگه می‌خواید محدودیت Foreign Key رو در سطح دیتابیس اعمال کنید، از کوئری خام SQL استفاده کنید.

بهترین روش برای بکاپ‌گیری از دیتابیس SQLite در .NET MAUI چیست؟

ساده‌ترین روش، کپی فایل .db3 از FileSystem.AppDataDirectory به مکان دیگه‌ای مثل حافظه ابری یا فضای اشتراکی دستگاهه. فقط قبل از کپی مطمئن بشید هیچ تراکنش فعالی در حال اجرا نیست. همچنین می‌تونید از دستور VACUUM INTO برای ساخت یک کپی فشرده‌شده از دیتابیس استفاده کنید.

درباره نویسنده Editorial Team

Our team of expert writers and editors.