Lokálna databáza v .NET MAUI: Kompletný sprievodca SQLite, CRUD operáciami a Repository vzorom

Naučte sa pracovať s lokálnou SQLite databázou v .NET MAUI 10 — od inštalácie cez CRUD operácie až po Repository vzor s dependency injection. Kompletný sprievodca s praktickými príkladmi.

Prečo každá mobilná aplikácia potrebuje lokálnu databázu

Ak ste sledovali našu sériu o .NET MAUI — od MVVM architektúry cez Shell navigáciu — máte solídny základ pre stavbu profesionálnych mobilných aplikácií. Lenže, čo s dátami? Každá aplikácia, ktorá stojí za zmienku, potrebuje niekde ukladať údaje. A presne tu prichádza na scénu SQLite.

SQLite je odľahčená, súborová relačná databáza bežiaca priamo na zariadení používateľa. Žiadny server, žiadna sieťová konektivita — jednoducho funguje. A funguje spoľahlivo na všetkých platformách — Android, iOS, macOS aj Windows. V .NET MAUI je to de facto štandard pre lokálne ukladanie štruktúrovaných dát.

V tomto článku si prejdeme naozaj všetko — od úplných základov (inštalácia balíčkov, konfigurácia pripojenia) až po pokročilé vzory ako Repository pattern s dependency injection. Praktické príklady priložené, takže môžete kód rovno použiť vo svojich projektoch.

Inštalácia a konfigurácia SQLite v .NET MAUI projekte

Krok 1: Pridanie NuGet balíčka

Pre prácu s SQLite v .NET MAUI použijete balíček sqlite-net-pcl. Názov je trochu mätúci (PCL — vážne?), ale je to presne ten správny balíček. Poskytuje jednoduchý ORM nad SQLite databázou a je odskúšaný v množstve produkčných aplikácií.

dotnet add package sqlite-net-pcl

Aktuálna stabilná verzia je 1.9.172, plne kompatibilná s .NET MAUI 10 a .NET 10.

Alternatívne môžete siahnuť po Microsoft.EntityFrameworkCore.Sqlite, ak preferujete plnohodnotný ORM s migráciami. My sa však zameriame na sqlite-net-pcl — je odľahčenejší, rýchlejší a úprimne, pre väčšinu mobilných aplikácií úplne dostačujúci.

Krok 2: Definovanie konštánt databázy

Vytvorte si triedu s konfiguračnými konštantami. Nič zložité, ale ušetrí vám to kopírovanie stringov po celom projekte:

public static class DatabaseConstants
{
    public const string DatabaseFilename = "mojaaplikacia.db3";

    public const SQLiteOpenFlags Flags =
        SQLiteOpenFlags.ReadWrite |
        SQLiteOpenFlags.Create |
        SQLiteOpenFlags.SharedCache;

    public static string DatabasePath =>
        Path.Combine(FileSystem.AppDataDirectory, DatabaseFilename);
}

Prečo práve tieto príznaky?

  • ReadWrite — umožňuje čítanie aj zápis dát
  • Create — automaticky vytvorí súbor databázy, ak ešte neexistuje
  • SharedCache — zdieľanie cache medzi viacerými pripojeniami, čo sa hodí pri viacvláknovom prístupe

Cesta k databáze používa FileSystem.AppDataDirectory — to je platforma-nezávislá cesta k priečinku aplikácie. Na každej platforme sa mapuje na správne miesto (na Androide napríklad do interného úložiska aplikácie).

Vytváranie modelov — definícia dátovej štruktúry

SQLite.NET používa atribúty na mapovanie tried na databázové tabuľky. Pozrime sa na konkrétny príklad — model pre klasickú Todo aplikáciu:

using SQLite;

[Table("tasks")]
public class TaskItem
{
    [PrimaryKey, AutoIncrement]
    [Column("id")]
    public int Id { get; set; }

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

    [Column("description")]
    public string? Description { get; set; }

    [Column("is_completed")]
    public bool IsCompleted { get; set; }

    [Column("priority")]
    public int Priority { get; set; }

    [Column("due_date")]
    public DateTime? DueDate { get; set; }

    [Column("created_at")]
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    [Column("updated_at")]
    public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

Tu je prehľad kľúčových atribútov, ktoré máte k dispozícii:

  • [PrimaryKey] a [AutoIncrement] — primárny kľúč s automatickým inkrementom
  • [Table("nazov")] — explicitne nastaví názov tabuľky
  • [Column("nazov")] — explicitne nastaví názov stĺpca
  • [MaxLength(n)] — obmedzí dĺžku textového poľa
  • [NotNull] — stĺpec nesmie byť null
  • [Indexed] — vytvorí index pre rýchlejšie vyhľadávanie
  • [Unique] — zabezpečí unikátnosť hodnôt
  • [Ignore] — vlastnosť sa nebude mapovať do databázy

Asynchrónne pripojenie s lazy inicializáciou

Toto je jeden z najdôležitejších aspektov práce s SQLite v mobilnej aplikácii. Databázové operácie sú I/O operácie a nikdy by nemali blokovať hlavné UI vlákno. Preto vždy používajte SQLiteAsyncConnection. Bez výnimky.

Správny vzor: Lazy inicializácia

Lazy inicializácia odkladá vytvorenie databázového pripojenia na moment, keď je skutočne potrebné. Databáza sa teda neotvorí počas štartu aplikácie — a to je presne to, čo chcete, pretože čas spustenia zostane na minime:

public class DatabaseService
{
    private SQLiteAsyncConnection? _database;

    private async Task<SQLiteAsyncConnection> GetDatabaseAsync()
    {
        if (_database is not null)
            return _database;

        _database = new SQLiteAsyncConnection(
            DatabaseConstants.DatabasePath,
            DatabaseConstants.Flags);

        // Povolenie Write-Ahead Logging pre lepší výkon
        await _database.EnableWriteAheadLoggingAsync();

        // Vytvorenie tabuliek — bezpečné volať opakovane
        await _database.CreateTableAsync<TaskItem>();

        return _database;
    }
}

Dobrá správa — metóda CreateTableAsync je bezpečná na opakované volanie. Ak tabuľka už existuje, jednoducho ju preskočí. Takže ju môžete pokojne volať pri každom prístupe k databáze.

Prečo Write-Ahead Logging (WAL)?

WAL je režim žurnálovania, ktorý zapisuje zmeny do samostatného WAL súboru pred ich aplikovaním do hlavnej databázy. Hlavné výhody sú tri:

  • Súčasné čítanie a zápis — čitatelia neblokujú zapisovateľov a naopak
  • Rýchlejšie commitovanie — zmeny sa zapisujú sekvenčne do jedného súboru
  • Lepší výkon pri viacerých transakciách — ideálne pre mobilné aplikácie s paralelnými operáciami

CRUD operácie — vytvorenie, čítanie, aktualizácia, mazanie

Tak, teraz sa dostávame k jadru celej veci. Poďme si prejsť všetky základné operácie jednu po druhej.

Create — vloženie nového záznamu

public async Task<int> CreateTaskAsync(TaskItem task)
{
    var db = await GetDatabaseAsync();
    task.CreatedAt = DateTime.UtcNow;
    task.UpdatedAt = DateTime.UtcNow;
    return await db.InsertAsync(task);
}

// Hromadné vkladanie
public async Task<int> CreateTasksAsync(IEnumerable<TaskItem> tasks)
{
    var db = await GetDatabaseAsync();
    return await db.InsertAllAsync(tasks);
}

Metóda InsertAsync vracia počet vložených riadkov. A čo je šikovné — po úspešnom vložení sa vlastnosť Id objektu automaticky naplní vygenerovaným primárnym kľúčom. Nemusíte robiť ďalší dotaz.

Read — čítanie dát

// Získanie všetkých úloh
public async Task<List<TaskItem>> GetAllTasksAsync()
{
    var db = await GetDatabaseAsync();
    return await db.Table<TaskItem>()
        .OrderByDescending(t => t.CreatedAt)
        .ToListAsync();
}

// Získanie jednej úlohy podľa ID
public async Task<TaskItem?> GetTaskByIdAsync(int id)
{
    var db = await GetDatabaseAsync();
    return await db.Table<TaskItem>()
        .Where(t => t.Id == id)
        .FirstOrDefaultAsync();
}

// Filtrovanie — nedokončené úlohy zoradené podľa priority
public async Task<List<TaskItem>> GetPendingTasksAsync()
{
    var db = await GetDatabaseAsync();
    return await db.Table<TaskItem>()
        .Where(t => !t.IsCompleted)
        .OrderBy(t => t.Priority)
        .ThenBy(t => t.DueDate)
        .ToListAsync();
}

// Vyhľadávanie pomocou SQL dotazu
public async Task<List<TaskItem>> SearchTasksAsync(string searchTerm)
{
    var db = await GetDatabaseAsync();
    return await db.QueryAsync<TaskItem>(
        "SELECT * FROM tasks WHERE title LIKE ? OR description LIKE ?",
        $"%{searchTerm}%", $"%{searchTerm}%");
}

SQLite.NET podporuje LINQ-like syntax cez metódu Table<T>(), čo je čitateľné a typovo bezpečné. Pre zložitejšie dotazy (napríklad full-text search alebo JOINy) môžete použiť priame SQL cez QueryAsync.

Update — aktualizácia záznamu

public async Task<int> UpdateTaskAsync(TaskItem task)
{
    var db = await GetDatabaseAsync();
    task.UpdatedAt = DateTime.UtcNow;
    return await db.UpdateAsync(task);
}

// Označenie úlohy ako dokončenej
public async Task ToggleTaskCompletionAsync(int taskId)
{
    var db = await GetDatabaseAsync();
    var task = await GetTaskByIdAsync(taskId);
    if (task is null) return;

    task.IsCompleted = !task.IsCompleted;
    task.UpdatedAt = DateTime.UtcNow;
    await db.UpdateAsync(task);
}

UpdateAsync identifikuje záznam podľa primárneho kľúča a aktualizuje všetky ostatné stĺpce. Jednoduché a priamočiare.

Delete — mazanie záznamu

public async Task<int> DeleteTaskAsync(TaskItem task)
{
    var db = await GetDatabaseAsync();
    return await db.DeleteAsync(task);
}

// Mazanie podľa ID
public async Task<int> DeleteTaskByIdAsync(int id)
{
    var db = await GetDatabaseAsync();
    return await db.DeleteAsync<TaskItem>(id);
}

// Vymazanie všetkých dokončených úloh
public async Task<int> DeleteCompletedTasksAsync()
{
    var db = await GetDatabaseAsync();
    return await db.ExecuteAsync(
        "DELETE FROM tasks WHERE is_completed = ?", true);
}

Repository vzor — abstrakcia dátového prístupu

Ak ste čítali náš článok o MVVM architektúre, viete, že oddeľovanie zodpovedností je základ všetkého. Repository vzor pridáva ďalšiu vrstvu abstrakcie medzi ViewModely a databázu. A úprimne? Je to jedna z najlepších investícií do architektúry vašej aplikácie.

Prečo?

  • Testovateľnosť — vo ViewModeloch jednoducho nahradíte skutočnú databázu mock implementáciou
  • Flexibilita — dnes SQLite, zajtra REST API — ViewModely sa nemusia vôbec zmeniť
  • Čistý kód — ViewModely neobsahujú žiadnu databázovú logiku

Definícia rozhrania

public interface ITaskRepository
{
    Task<List<TaskItem>> GetAllAsync();
    Task<TaskItem?> GetByIdAsync(int id);
    Task<List<TaskItem>> GetPendingAsync();
    Task<List<TaskItem>> SearchAsync(string searchTerm);
    Task<int> CreateAsync(TaskItem task);
    Task<int> UpdateAsync(TaskItem task);
    Task<int> DeleteAsync(int id);
    Task<int> DeleteCompletedAsync();
}

Implementácia s SQLite

public class SqliteTaskRepository : ITaskRepository
{
    private SQLiteAsyncConnection? _database;

    private async Task<SQLiteAsyncConnection> GetDatabaseAsync()
    {
        if (_database is not null)
            return _database;

        _database = new SQLiteAsyncConnection(
            DatabaseConstants.DatabasePath,
            DatabaseConstants.Flags);

        await _database.EnableWriteAheadLoggingAsync();
        await _database.CreateTableAsync<TaskItem>();

        return _database;
    }

    public async Task<List<TaskItem>> GetAllAsync()
    {
        var db = await GetDatabaseAsync();
        return await db.Table<TaskItem>()
            .OrderByDescending(t => t.CreatedAt)
            .ToListAsync();
    }

    public async Task<TaskItem?> GetByIdAsync(int id)
    {
        var db = await GetDatabaseAsync();
        return await db.Table<TaskItem>()
            .Where(t => t.Id == id)
            .FirstOrDefaultAsync();
    }

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

    public async Task<List<TaskItem>> SearchAsync(string searchTerm)
    {
        var db = await GetDatabaseAsync();
        return await db.QueryAsync<TaskItem>(
            "SELECT * FROM tasks WHERE title LIKE ? OR description LIKE ?",
            $"%{searchTerm}%", $"%{searchTerm}%");
    }

    public async Task<int> CreateAsync(TaskItem task)
    {
        var db = await GetDatabaseAsync();
        task.CreatedAt = DateTime.UtcNow;
        task.UpdatedAt = DateTime.UtcNow;
        return await db.InsertAsync(task);
    }

    public async Task<int> UpdateAsync(TaskItem task)
    {
        var db = await GetDatabaseAsync();
        task.UpdatedAt = DateTime.UtcNow;
        return await db.UpdateAsync(task);
    }

    public async Task<int> DeleteAsync(int id)
    {
        var db = await GetDatabaseAsync();
        return await db.DeleteAsync<TaskItem>(id);
    }

    public async Task<int> DeleteCompletedAsync()
    {
        var db = await GetDatabaseAsync();
        return await db.ExecuteAsync(
            "DELETE FROM tasks WHERE is_completed = ?", true);
    }
}

Dependency Injection — registrácia v MauiProgram.cs

Repository zaregistrujte ako singleton v MauiProgram.cs. Prečo singleton? Pretože chcete zdieľať jedno databázové pripojenie v celej aplikácii — vytváranie nových pripojení pri každej operácii by bolo zbytočne nákladné:

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

        // Registrácia repozitára ako singleton
        builder.Services.AddSingleton<ITaskRepository, SqliteTaskRepository>();

        // Registrácia ViewModelov
        builder.Services.AddTransient<TaskListViewModel>();
        builder.Services.AddTransient<TaskDetailViewModel>();

        // Registrácia stránok
        builder.Services.AddTransient<TaskListPage>();
        builder.Services.AddTransient<TaskDetailPage>();

        return builder.Build();
    }
}

Všimnite si, že ViewModely sú registrované ako transient. To znamená, že sa vytvárajú zakaždým nanovo pri navigácii na danú stránku, čím dostanete vždy čistý stav. Repozitár naopak žije po celú dobu behu aplikácie.

Integrácia s MVVM a CommunityToolkit

Teraz spojíme všetko dohromady — Repository vzor, dependency injection a MVVM s CommunityToolkit.Mvvm. Ak ste čítali náš článok o MVVM architektúre, tieto vzory vám budú povedomé. Ak nie, nevadí — kód je dosť popisný sám o sebe.

TaskListViewModel — zobrazenie zoznamu úloh

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

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

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

    [ObservableProperty]
    private ObservableCollection<TaskItem> tasks = new();

    [ObservableProperty]
    private bool isRefreshing;

    [ObservableProperty]
    private string searchText = string.Empty;

    [RelayCommand]
    private async Task LoadTasksAsync()
    {
        try
        {
            IsRefreshing = true;
            var items = string.IsNullOrWhiteSpace(SearchText)
                ? await _taskRepository.GetAllAsync()
                : await _taskRepository.SearchAsync(SearchText);

            Tasks = new ObservableCollection<TaskItem>(items);
        }
        finally
        {
            IsRefreshing = false;
        }
    }

    [RelayCommand]
    private async Task AddTaskAsync()
    {
        await Shell.Current.GoToAsync("task-detail");
    }

    [RelayCommand]
    private async Task ToggleCompletedAsync(TaskItem task)
    {
        task.IsCompleted = !task.IsCompleted;
        await _taskRepository.UpdateAsync(task);
        await LoadTasksAsync();
    }

    [RelayCommand]
    private async Task DeleteTaskAsync(TaskItem task)
    {
        bool confirm = await Shell.Current.DisplayAlert(
            "Vymazať úlohu",
            $"Naozaj chcete vymazať úlohu \"{task.Title}\"?",
            "Áno", "Nie");

        if (!confirm) return;

        await _taskRepository.DeleteAsync(task.Id);
        Tasks.Remove(task);
    }
}

TaskListPage — XAML stránka so zoznamom

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:MojaAplikacia.ViewModels"
             xmlns:models="clr-namespace:MojaAplikacia.Models"
             x:Class="MojaAplikacia.Views.TaskListPage"
             x:DataType="vm:TaskListViewModel"
             Title="Moje úlohy">

    <Grid RowDefinitions="Auto,*" Padding="16">

        <!-- Vyhľadávanie -->
        <SearchBar Grid.Row="0"
                   Text="{Binding SearchText}"
                   SearchCommand="{Binding LoadTasksCommand}"
                   Placeholder="Hľadať úlohy..." />

        <!-- Zoznam úloh -->
        <RefreshView Grid.Row="1"
                     IsRefreshing="{Binding IsRefreshing}"
                     Command="{Binding LoadTasksCommand}">

            <CollectionView ItemsSource="{Binding Tasks}"
                            ItemSizingStrategy="MeasureFirstItem"
                            SelectionMode="None">

                <CollectionView.EmptyView>
                    <Label Text="Žiadne úlohy. Pridajte novú!"
                           HorizontalOptions="Center"
                           VerticalOptions="Center"
                           FontSize="16"
                           TextColor="Gray" />
                </CollectionView.EmptyView>

                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="models:TaskItem">
                        <SwipeView>
                            <SwipeView.RightItems>
                                <SwipeItems>
                                    <SwipeItem Text="Vymazať"
                                               BackgroundColor="Red"
                                               Command="{Binding Source={RelativeSource AncestorType={x:Type vm:TaskListViewModel}}, Path=DeleteTaskCommand}"
                                               CommandParameter="{Binding .}" />
                                </SwipeItems>
                            </SwipeView.RightItems>

                            <Border Stroke="LightGray"
                                    StrokeThickness="1"
                                    StrokeShape="RoundRectangle 8"
                                    Padding="12"
                                    Margin="0,4">
                                <Grid ColumnDefinitions="Auto,*,Auto">
                                    <CheckBox Grid.Column="0"
                                              IsChecked="{Binding IsCompleted}"
                                              CheckedChanged="OnTaskCheckedChanged" />
                                    <VerticalStackLayout Grid.Column="1" Spacing="4">
                                        <Label Text="{Binding Title}"
                                               FontSize="16"
                                               FontAttributes="Bold" />
                                        <Label Text="{Binding Description}"
                                               FontSize="13"
                                               TextColor="Gray"
                                               LineBreakMode="TailTruncation" />
                                    </VerticalStackLayout>
                                    <Label Grid.Column="2"
                                           Text="{Binding DueDate, StringFormat='{0:dd.MM}'}"
                                           FontSize="12"
                                           TextColor="Gray"
                                           VerticalOptions="Center" />
                                </Grid>
                            </Border>
                        </SwipeView>
                    </DataTemplate>
                </CollectionView.ItemTemplate>

            </CollectionView>
        </RefreshView>
    </Grid>

</ContentPage>

Pár vecí, ktoré si tu oplatí všimnúť:

  • x:DataType — compiled bindings pre lepší výkon (žiadna reflexia za behu)
  • ItemSizingStrategy="MeasureFirstItem" — optimalizácia pre CollectionView, keď majú položky rovnakú výšku
  • Border namiesto Frame — Border je efektívnejší kontajner (Frame je v podstate legacy)
  • SwipeView — natívne gesto pre mazanie potiahnutím, ktoré používatelia poznajú z iných aplikácií

Pokročilé techniky — transakcie a dávkové operácie

Pre operácie, kde potrebujete atomicitu (buď sa vykonajú všetky zmeny, alebo žiadna), použite transakcie. Toto je kritické napríklad pri reorganizácii úloh:

public async Task ReorganizeTasksAsync(
    List<TaskItem> tasksToUpdate,
    List<TaskItem> tasksToDelete)
{
    var db = await GetDatabaseAsync();

    await db.RunInTransactionAsync(transaction =>
    {
        foreach (var task in tasksToUpdate)
        {
            task.UpdatedAt = DateTime.UtcNow;
            transaction.Update(task);
        }

        foreach (var task in tasksToDelete)
        {
            transaction.Delete(task);
        }
    });
}

Transakcie majú ešte jednu veľkú výhodu, o ktorej sa menej hovorí — výkon. Bez transakcie každý INSERT alebo UPDATE vykonáva samostatný disk commit. V transakcii sa všetky zmeny commitujú naraz. Pri dávkových operáciách (povedzme 100+ záznamov) je rozdiel dramatický.

Platformové špecifiká a bežné problémy

iOS — pozor na linker

Na iOS môže linker odstrániť typy, ktoré sa zdajú byť nepoužívané. Ak vám aplikácia padá pri prístupe k SQLite (a na Androide pritom funguje bez problémov), pravdepodobne je na vine práve linker. Riešenie je jednoduché:

[Preserve(AllMembers = true)]
[Table("tasks")]
public class TaskItem
{
    // ...
}

Alternatívne nakonfigurujte linker v súbore Platforms/iOS/LinkerConfig.xml.

Android — šifrovanie databázy

Pre citlivé dáta zvážte šifrovanie databázy pomocou balíčka SQLitePCLRaw.bundle_e_sqlcipher:

var db = new SQLiteAsyncConnection(
    DatabaseConstants.DatabasePath,
    DatabaseConstants.Flags,
    storeDateTimeAsTicks: true);

// Pri použití SQLCipher
// await db.ExecuteAsync("PRAGMA key = 'vase-heslo'");

Spoločný problém: súbežný prístup

SQLite podporuje viacero čitateľov súčasne, ale len jedného zapisovateľa. Ak vaša aplikácia vykonáva veľa zápisov z rôznych vlákien, môžete naraziť na nepríjemnú chybu database is locked. Riešenie v troch krokoch:

  • Používať jeden singleton SQLiteAsyncConnection — náš Repository vzor to zabezpečuje automaticky
  • Povoliť WAL režim — to sme už urobili vyššie
  • Pre kritické sekcie so zápisom použiť SemaphoreSlim

Tipy pre výkon SQLite v mobilnej aplikácii

Na záver niekoľko tipov z praxe, ktoré vám môžu ušetriť hodiny ladenia výkonu:

  1. Indexy na často filtrované stĺpce — pridajte [Indexed] atribút na stĺpce, podľa ktorých často vyhľadávate. Rozdiel v rýchlosti je obrovský, hlavne pri väčších datasetoch.
  2. Dávkové vkladanie v transakcii — namiesto 100 jednotlivých INSERTov zabaľte všetko do jednej transakcie
  3. Lazy loading a stránkovanie — nenačítavajte celú databázu do pamäte naraz
  4. Compiled bindings v CollectionView — vždy nastavte x:DataType na DataTemplate
  5. Obmedzte PropertyChanged notifikácie — pri hromadných aktualizáciách nastavte vlastnosti pred pridaním do ObservableCollection
// Príklad stránkovania
public async Task<List<TaskItem>> GetTasksPagedAsync(int page, int pageSize = 20)
{
    var db = await GetDatabaseAsync();
    return await db.Table<TaskItem>()
        .OrderByDescending(t => t.CreatedAt)
        .Skip(page * pageSize)
        .Take(pageSize)
        .ToListAsync();
}

Migrácia schémy — pridávanie nových stĺpcov

S rastom aplikácie budete nevyhnutne potrebovať upraviť databázovú schému. SQLite.NET nemá vstavaný migračný systém ako Entity Framework, ale existuje pomerne elegantný spôsob, ako to zvládnuť ručne:

private async Task MigrateDatabaseAsync(SQLiteAsyncConnection db)
{
    // Získanie informácií o existujúcich stĺpcoch
    var tableInfo = await db.GetTableInfoAsync("tasks");
    var columnNames = tableInfo.Select(c => c.Name).ToHashSet();

    // Pridanie nového stĺpca, ak neexistuje
    if (!columnNames.Contains("category"))
    {
        await db.ExecuteAsync(
            "ALTER TABLE tasks ADD COLUMN category TEXT DEFAULT 'general'");
    }

    if (!columnNames.Contains("reminder_at"))
    {
        await db.ExecuteAsync(
            "ALTER TABLE tasks ADD COLUMN reminder_at TEXT");
    }
}

Tento prístup skontroluje existujúce stĺpce a pridá len tie, ktoré chýbajú. Volajte túto metódu v rámci GetDatabaseAsync() po vytvorení tabuliek — je bezpečné ju spúšťať opakovane.

Často kladené otázky

Aký je rozdiel medzi sqlite-net-pcl a Entity Framework Core pre .NET MAUI?

sqlite-net-pcl je odľahčený ORM navrhnutý priamo pre mobilné aplikácie — je rýchlejší, má menšiu veľkosť a jednoduchšiu API. Entity Framework Core je plnohodnotný ORM so všetkým, čo k tomu patrí: migrácie, zložité dotazy, pokročilé relačné mapovanie. Pre väčšinu mobilných aplikácií je sqlite-net-pcl lepšia voľba. Po EF Core siahnite vtedy, ak naozaj potrebujete komplexné relačné mapovanie alebo automatické migrácie.

Kde sa ukladá SQLite databáza na zariadení?

Závisí od platformy, ale pomocou FileSystem.AppDataDirectory získate správnu cestu automaticky. Na Androide je to interné úložisko aplikácie (/data/data/com.vasa.app/files/), na iOS priečinok Library. Obe umiestnenia sú privátne a prístupné len vašej aplikácii.

Ako zálohovať SQLite databázu v .NET MAUI aplikácii?

Najjednoduchšie je skopírovať súbor .db3. Pred kopírovaním sa len uistite, že žiadna operácia práve nepíše do databázy. Ďalšia možnosť je implementovať export dát do JSON formátu — to umožní aj prenos medzi platformami.

Je SQLite bezpečné pre ukladanie citlivých údajov?

V predvolenom nastavení nie — SQLite databáza nie je šifrovaná. Pre citlivé údaje použite SQLCipher cez balíček SQLitePCLRaw.bundle_e_sqlcipher, ktorý poskytuje AES-256 šifrovanie celej databázy. Pre menšie objemy citlivých dát (heslá, tokeny) je lepšia voľba SecureStorage z .NET MAUI Essentials.

Ako riešiť konflikty pri súčasnom prístupe z viacerých vlákien?

Kľúčové je používať jeden singleton SQLiteAsyncConnection — náš Repository vzor to zabezpečuje. K tomu zapnite WAL režim. Pre kritické sekcie so zápisom môžete pridať SemaphoreSlim. SQLite samotné garantuje atomicitu jednotlivých operácií, ale pre skupiny súvisiacich operácií vždy používajte transakcie.

O Autorovi Editorial Team

Our team of expert writers and editors.